<!DOCTYPE html>
<html>
<head>
  <title>Talking Head</title>

  <meta charset="utf-8">
  <meta content="width=device-width, height=device-height, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0, user-scalable=no, interactive-widget=resizes-content" name="viewport">
  <link rel="icon" type="image/jpg" href="favicon.jpg">

  <style>

  html {
    width:100%; height:100%; height: -webkit-fill-available; font-size: 14px;
  }

  /* Color themes */
  :root, .theme-dark {
    --colorBody: #88ccff;
    --colorBackground: #202020;
    --colorPanel: rgba(32,32,32,0.98);
    --colorBorderPassive: rgba(136,204,255,0.6);
    --colorBorderActive: #88ccff;
    --colorUserText: #88ccff;
    --colorSystemText: #cc77ff;
    --colorToolbarPassive: rgba(136,204,255,0.6);
    --colorToolbarPassive2: rgba(136,204,255,0.3);
    --colorToolbarActive: #88ccff;
    --widthLabel: 5rem;
    --colorGlow: #88ccff;
  }
  .theme-light {
    --colorBody: #505050;
    --colorBackground: #f0f0f0;
    --colorPanel: rgba(224,224,224,0.98);
    --colorBorderPassive: #d0d0d0;
    --colorBorderActive: #d0d0d0;
    --colorUserText: #404444;
    --colorSystemText: #678fb9;
    --colorToolbarPassive: rgba(80,80,80,0.75);
    --colorToolbarPassive2: rgba(80,80,80,0.5);
    --colorToolbarActive: #606666;
    --widthLabel: 5rem;
  }

  body {
    width: 100%; height: 100%; min-height: 100vh;
    min-height: -webkit-fill-available; margin: 0; padding: 0;
    background-color: var(--colorBackground); color: var(--colorBody);
    overflow: hidden; text-align: left; touch-action: manipulation;
    font-family: "SansRegular", sans-serif; font-size: 1.4rem;
    line-height: 1.4rem; scrollbar-color: var(--colorToolbarPassive) transparent;
    scrollbar-width: 20px; scrollbar-height: 20px;
  }

  ::-webkit-scrollbar { width: 20px; height: 20px; }
  ::-webkit-scrollbar-track {
    background: linear-gradient(
      to right, transparent,
      transparent 9px,
      var(--colorToolbarPassive) 9px,
      var(--colorToolbarPassive) 11px,
      transparent 11px
    );
  }
  ::-webkit-scrollbar-thumb {
    background-color: var(--colorPanel); border-radius: 10px;
    border: 2px solid var(--colorToolbarPassive);
  }

  input[type="range"] {
    position: relative; width: 180px; height: 0.8rem; line-height: 2rem;
    -webkit-appearance: none; appearance: none; margin-top: 0.6rem;
    cursor: pointer; pointer-events: auto;
    background: repeating-linear-gradient(
      to right, var(--colorToolbarPassive),
      var(--colorToolbarPassive) 2px,
      transparent 2px,
      transparent 44.5px
    );
  }
  input[type="range"]::-webkit-slider-runnable-track {
    background: var(--colorToolbarPassive); height: 2px;
  }
  input[type="range"]::-moz-range-track {
    background: var(--colorToolbarPassive); height: 2px;
  }
  input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    background: var(--colorToolbarActive);
    margin-top: -0.6rem; height: 1.4rem; width: 0.8rem; border-radius: 0.1rem;
  }
  input[type="range"]::-moz-range-thumb {
    border: none; background-color: var(--colorToolbarActive); height: 1.2rem;
    width: 0.8rem; border-radius: 0.1rem;
  }
  input[type="range"]:focus { outline: none; }
  input[type="range"]:focus::-webkit-slider-thumb { outline: none; }
  input[type="range"]:focus::-moz-range-thumb { outline: none; }

  input[type="text"] {
    flex: 1; background-color: transparent; color: var(--colorUserText);
    border: 0; outline: none; line-height: 2rem; font-family: "SansRegular";
    font-size: 1.4rem; cursor: pointer; pointer-events: auto; margin: 0;
    padding: 0 6px;
  }
  input[type="text"]::placeholder {
    color: var(--colorUserText); opacity: 0.3;
  }
  input[type="color"] { background: none; border: 0; }

  /* Pages */
  .pages {
    position: absolute; top: 18px; left: 30px; right: 30px; bottom: 18px;
    display: flex; flex-direction: column; row-gap: 6px;
  }
  .row { display: flex; row-gap: 0; column-gap: 8px; }
  .column { flex: 1; display: flex; flex-direction: column; row-gap: 2px; }
  .page {
    flex-grow: 1; display: flex; flex-direction: column;
    overflow-y: auto; overflow-x: hidden; min-height: 0; margin: 8px 0 8px 0;
    padding-right: 16px;
  }
  .rowWrap {
    display: flex; row-gap: 2px; column-gap: 8px; flex-wrap: wrap;
  }
  .vspace {
    flex-shrink: 0; height: 1rem;
  }
  .vbar {
    height: 1px; background-color: var(--colorToolbarPassive2);
    flex-shrink: 0; margin: 1rem 0 1rem 0;
  }
  .fill, .filler {
    flex-shrink: 0; flex-grow: 1; flex-basis: 0; min-width: 0;
  }
  .filler { visibility: hidden; pointer-events: none; }
  .command, .text {
    display: inline-flex; height: calc( 2rem + 2px); font-size: 1.4rem;
    line-height: calc( 2rem + 2px); padding: 0 6px; gap: 12px;
    vertical-align: middle; text-align: left; font-family: "SansRegularCondensed";
    color: var(--colorToolbarPassive); white-space: nowrap; overflow: hidden;
  }
  .command, .text {
    font-family: "SansRegularCondensed"; color: var(--colorToolbarPassive);
  }
  .command { cursor: pointer; pointer-events: auto; }
  .command > svg, .text > svg {
    height: 2rem; width: 2rem; padding: 1px 0;
    min-height: 2rem; min-width: 2rem;
  }
  .label {
    width: var(--widthLabel); flex-shrink: 0; color: var(--colorToolbarPassive2);
  }
  .border {
    position: absolute; top: 0; left: 0; bottom: 0; right: 0;
    background: transparent; border-width: 3px; border-style: solid;
    border-color: var(--colorBorderActive); pointer-events: none;
    border-radius: inherit;
  }
  .glow { box-shadow: 0 0 25px 5px var(--colorGlow); }
  .theme-light .glow { box-shadow: none; }

  /* Main */
  #main {
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%,-50%); background-color: transparent;
  }

  /* Left panel */
  #left {
    position: absolute; margin: 4px; pointer-events: none;
    background-color: var(--colorPanel);
  }
  #avatar, #view {
    position: absolute; opacity: 0; top: 0; left: 0; right: 0;
    bottom: 0; pointer-events: auto; overflow: hidden;
    border-radius: inherit;
  }
  #view {
    background-image: none; background-repeat:no-repeat;
    background-position: center center; background-size: cover;
  }
  #video {
    position: absolute; height: 100%; width: 100%;
    object-position: center center; object-fit: cover;
  }

  /* Loading */
  #loading {
    display: block; position: absolute; top: 6px; left: 6px; width: 300px;
    height: 30px; pointer-events: none; font-size: 1rem;
    background-color: transparent;
  }
  #loading-back, #loading-top {
    display: block; position: absolute; top: 10px; left: 20px; bottom: 10px;
    width: 95px;
  }
  #loading-back {
    background: repeating-linear-gradient(
      to right, var(--colorToolbarPassive2),
      var(--colorToolbarPassive) 5px,
      transparent 5px,
      transparent 10px
    );
  }
  #loading-top {
    clip-path: inset(0 100% 0 0);
    background: repeating-linear-gradient(
      to right, var(--colorBody),
      var(--colorBody) 5px,
      transparent 5px,
      transparent 10px
    );
  }
  #loading-value {
    display: block; position: absolute; top: 0; left: 125px; bottom: 0;
    right: 0; line-height: 30px; text-align: left; font-size: 1.4rem;
    font-family: "SansRegularCondensed";
  }


  /* Right panel */
  #right {
    position: absolute; margin: 4px;
    background-color: var(--colorPanel);
  }
  #right .border { border-color: var(--colorBorderPassive); }
  .session { display: none; width: 100%; }
  .session.selected { display: block; }
  .message {
    position: relative; display: block; width: 100%; min-height: 2rem;
  }
  .message * {
    text-align:left; font-size: 1.4rem; line-height: 2rem;
  }
  .message > * {
    display: block; padding: 0 calc(6rem + 8px) 0 calc(4rem + 4px);
    color: var(--colorSystemText);  z-index: 10;
  }
  .message ul, .message ol { margin: 0 0 0 2rem; }
  .message.user > * { color: var(--colorUserText); }
  .message > .toolbar-left {
    position: absolute; left:0; bottom:0; padding: 0;
    display: flex; row-gap: 0; column-gap: 4px;
  }
  .message > .toolbar-right {
    position: absolute; right: 0; bottom:0; padding: 0;
    display: flex; row-gap: 0; column-gap: 4px;
  }
  code { font-family: monospace; font-size: 1rem; }
  #directory div:first-child div[data-entry-move="up"] { visibility: hidden; }
  #directory div:last-child div[data-entry-move="down"] { visibility: hidden; }
  .apikey, #right textarea {
    flex: 1; min-height: 2rem; margin: auto; background: transparent;
    border: 0; font-family: "SansRegular"; font-size: 1.4rem; padding: 0 6px;
    line-height: 2rem; resize: none; outline: none; color: var(--colorUserText);
    pointer-events: auto;
  }
  .apikey { -webkit-text-security: square; }
  #right textarea[data-item="ai-openai-ai1"], #right textarea[data-item="ai-openai-ai2"],
  #right textarea[data-item="ai-gemini-ai1"], #right textarea[data-item="ai-gemini-ai2"] {
    color: var(--colorSystemText);
  }
  .apikey::placeholder, #right textarea::placeholder { color: inherit; opacity: 0.3; }
  .emoji { cursor: pointer; pointer-events: auto; }
  #morphs .label { width: calc( 2.5 * var(--widthLabel) ); }
  #right-sessions { scrollbar-gutter: stable; }

  /* Bottom panel */
  #bottom {
    position: absolute; margin: 4px; padding: 25px 25px 60px 25px;
    background-color: var(--colorPanel);
  }
  #bottom .border { border-color: var(--colorBorderPassive); }
  #bottom textarea {
    flex: 1; min-height: 2rem; margin: 0; background: transparent;
    border: 0; font-family: "SansRegular"; font-size: 1.4rem;
    line-height: 2rem; resize: none; outline: none; color: var(--colorUserText);
    pointer-events: auto; box-sizing: border-box;
  }
  #bottom textarea::placeholder { color: var(--colorUserText); opacity: 0.3; }
  #flag, #score, #scripttag, #scriptstatus { color: var(--colorToolbarPassive2); }

  [data-image-type^='video/']:before {
    content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='16' height='16' viewBox='-10 -10 58 58'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M43,42H5c-2.209,0-4-1.791-4-4V10c0-2.209,1.791-4,4-4h38c2.209,0,4,1.791,4,4v28 C47,40.209,45.209,42,43,42z M12,8H5c-1.104,0-2,0.896-2,2v2h9V8z M23,8h-9v4h9V8z M34,8h-9v4h9V8z M45,10c0-1.104-0.896-2-2-2h-7v4 h9l0,0V10z M45,14L45,14H3v20h42l0,0V14z M45,36L45,36h-9v4h-2v-4h-9v4h-2v-4h-9v4h-2v-4H3v2c0,1.104,0.896,2,2,2h38 c1.104,0,2-0.896,2-2V36z M21.621,29.765C21.449,29.904,21.238,30,21,30c-0.553,0-1-0.447-1-1V19c0-0.552,0.447-1,1-1 c0.213,0,0.4,0.082,0.563,0.196l7.771,4.872C29.72,23.205,30,23.566,30,24c0,0.325-0.165,0.601-0.405,0.783L21.621,29.765z' fill='gray'%3E%3C/path%3E%3C/svg%3E");
    margin-right: 4px;
  }

  /* Misc */
  .text.selected, .command.selected, .screen:active, .command:active,
  .range:active , .emoji.selected, .emoji:active {
    color: var(--colorToolbarActive); filter: drop-shadow(0 0 8px var(--colorGlow));
  }
  .theme-light .text.selected, .theme-light .command.selected,
  .theme-light .screen:active, .theme-light .command:active,
  .theme-light .range:active, .theme-light .emoji.selected,
  .theme-light .emoji:active {
    background-color: var(--colorToolbarActive); color: var(--colorPanel);
    filter: none; border-radius: 6px;
  }
  .grayed { opacity: 0.3; }
  .noselect {
    -webkit-touch-callout: none; -webkit-user-select: none;
    -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none;
    user-select: none; -webkit-user-select: none;
  }
  .nodrag {
    -webkit-user-drag: none; -khtml-user-drag: none; -moz-user-drag: none;
    -o-user-drag: none; -ms-user-drag: none; user-drag: none;
  }
  .disabled { opacity: 0.5; cursor: default; pointer-events: none; }
  .hidden { display: none; }
  .starttransparent { opacity: 0; }

  /* blinking */
  @-webkit-keyframes blinking { to { visibility: hidden } }
  @keyframes blinking { to { visibility: hidden } }
  .blink>:not(div):not(ol):not(ul):not(pre):last-child:after,
  .blink>ol:last-child li:last-child:after,.blink>pre:last-child code:after,
  .blink>ul:last-child li:last-child:after {
    -webkit-animation: blinking 1s steps(5,start) infinite;
    animation: blinking 1s steps(5,start) infinite; content: "▋";
    position: absolute; margin: 0 0 0 .25rem; padding:0; vertical-align: baseline;
    white-space: nowrap; width: 0; overflow: visible;
  }


  /* Fonts */
  @font-face {
    font-family:"SansRegular";
    src: url("./fonts/FiraSansCondensed-Regular.ttf") format('truetype');
  }
  @font-face {
    font-family:"SansItalic";
    src: url("./fonts/FiraSansCondensed-Italic.ttf") format('truetype');
  }
  @font-face {
    font-family:"SansBold";
    src: url("./fonts/FiraSansCondensed-Bold.ttf") format('truetype');
  }
  @font-face {
    font-family:"SansBoldItalic";
    src: url("./fonts/FiraSansCondensed-BoldItalic.ttf") format('truetype');
  }
  @font-face {
    font-family:"SansRegularCondensed";
    src: url("./fonts/FiraSansExtraCondensed-Regular.ttf") format('truetype');
  }
  @font-face {
    font-family:"SansBoldCondensed";
    src: url("./fonts/FiraSansExtraCondensed-Bold.ttf") format('truetype');
  }

  /* Layouts */
  #main.ratio-wide { width: min(96vw, 192vh, 1400px); height: min(48vw,96vh,700px); } /* 1:2 */
  #main.ratio-normal { width: min(96vw, 128vh, 1040px); height: min(72vw,96vh,780px); } /* 3:4 */
  #main.layout-full { width: 100%; height: 100%; }
  #main.layout-land #left { top: 0; left: 0; bottom: 38%; right: 50%; }
  #main.layout-port #left { top: 0; left: 0; bottom: 0; right: 60%; }
  #main.layout-full #left { top: 0; left: 0; bottom: 0; right: 0; margin: 0; }
  #main.layout-full #left .border { display: none; }
  #main.layout-land #right { top: 0; left: 50%; bottom: 0; right: 0; }
  #main.layout-port #right { top: 0; left: 40%; bottom: 30%; right: 0; }
  #main.layout-full #right { top: 10%; left: 45%; bottom: 30%; right: 5%; max-width: 800px; }
  #main.layout-land #bottom { top: 62%; left: 0; bottom: 0; right: 50%; }
  #main.layout-port #bottom { top: 70%; left: 40%; bottom: 0; right: 0; }
  #main.layout-full #bottom { top: 70%; left: 45%; bottom: 10%; right: 5%; max-width: 750px; }
  #main.presence-vr #left { box-shadow: none; background-color: transparent; }
  #main.presence-vr #view { display: none; }
  #main.presence-vr #left .border { display: none; }
  #main.presence-vr #avatar { bottom: -8px; left: -400px; right: -400px; }
  #main.layout-full #avatar { top: 0; left: 0; bottom: 0; right: 45%; }

  @media (max-width: 1000px) {
    #main.ratio-wide, #main.ratio-normal { width: 96vw; height: 90vh; }
    #main.layout-full { width: 100%; height: 100%; }
    #main.layout-land #left { top: 0; left: 0; bottom: 62%; right: 0; }
    #main.layout-port #left { top: 0; left: 0; bottom: 24%; right: 62%; }
    #main.layout-land #right { top: 38%; left: 0; bottom: 24%; right: 0; }
    #main.layout-port #right { top: 0; left: 38%; bottom: 24%; right: 0; }
    #main.layout-full #right { top: 0; left: 38%; bottom: 24%; right: 0; max-width: none; }
    #main.layout-land #bottom { top: 76%; left: 0; bottom: 0; right: 0; }
    #main.layout-port #bottom { top: 76%; left: 0; bottom: 0; right: 0; }
    #main.layout-full #bottom { top: 76%; left: 38%; bottom: 0; right: 0; max-width: none; }
  }

  </style>

<script nomodule>alert("You browser doesn't seem to support modules. Use a modern browser to run Enterprise.");</script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script async src="https://cdn.jsdelivr.net/npm/es-module-shims@1.7.1/dist/es-module-shims.js"></script>
<script type="importmap">
{ "imports":
  {
    "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js/+esm",
    "three/examples/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/",
    "dompurify": "https://cdn.jsdelivr.net/npm/dompurify@3.1.7/+esm",
    "marked": "https://cdn.jsdelivr.net/npm/marked@14.1.3/+esm"
  }
}
</script>
<script src="https://cdn.jsdelivr.net/npm/microsoft-cognitiveservices-speech-sdk@latest/distrib/browser/microsoft.cognitiveservices.speech.sdk.bundle-min.js"></script>

<script type="module">
import { site } from './siteconfig.js'
import { TalkingHead } from "./modules/talkinghead.mjs";
import dompurify from 'dompurify';
import { marked } from 'marked';

// API proxys
const jwtEndpoint = "/app/jwt/get"; // Get JSON Web Token for Single Sign-On
const openaiChatCompletionsProxy = "/openai/v1/chat/completions";
const openaiModerationsProxy = "/openai/v1/moderations";
const openaiAudioTranscriptionsProxy = "/openai/v1/audio/transcriptions";
const geminiProxy = "/gemini/";
const googleTTSProxy = "/gtts/";
const elevenTTSProxy = [
  "wss://" + window.location.host + "/elevenlabs/",
  "/v1/text-to-speech/",
  "/stream-input?model_id=eleven_turbo_v2_5&output_format=pcm_22050&auto_mode=true&apply_text_normalization=off"
];
const microsoftTTSProxy = [
  "wss://" + window.location.host + "/mstts/",
  "/cognitiveservices/websocket/v1"
];
const grokChatCompletionsProxy = "/grok/v1/chat/completions"; // Grok-beta
const llamaChatCompletionsProxy = "/llama/v1/chat/completions"; // Local llama.cpp server proxy
const localWhisperCppProxy = "/whisper/"; // Local whisper.cpp server proxy


// Direct API endpoints
const openaiChatCompletionsEndpoint = "https://api.openai.com/v1/chat/completions";
const openaiModerationsEndpoint = "https://api.openai.com/v1/moderations";
const openaiAudioTranscriptionsEndpoint = "https://api.openai.com/v1/audio/transcriptions";
const geminiEndpoint = "https://generativelanguage.googleapis.com/v1beta/models/";
const googleTTSEndpoint = "https://eu-texttospeech.googleapis.com/v1beta1/text:synthesize";
const elevenTTSEndpoint = [
  "wss://api.elevenlabs.io/v1/text-to-speech/",
  "/stream-input?model_id=eleven_turbo_v2_5&output_format=pcm_22050&auto_mode=true&apply_text_normalization=off"
];
const microsoftTTSEndpoint = "wss://northeurope.tts.speech.microsoft.com/cognitiveservices/websocket/v1";
const grokChatCompletionsEndpoint = "https://api.x.ai/v1/chat/completions";


// I18n internationalization
const i18n = {

  'fi': {
    'Avatar': 'Hahmo', 'Camera': 'Kamera', 'Audio': 'Ääni', 'Manuscript': 'Käsikirjoitus',
    'Emotion': 'Tunne', 'Neutral': 'Perus', 'Happy': 'Ilo', 'Angry': 'Viha', 'Sad': 'Suru',
    'Fear': 'Pelko', 'Disgust': 'Inho', 'Love': 'Rakkaus', 'Sleep': 'Uni', 'Pose': 'Asento',
    'Action': 'Toiminta', 'Frame': 'Rajaus', 'Full': 'Kokovartalo', 'Upper': 'Yläosa',
    'Head': 'Pää', 'Director': 'Ohjaus', 'Pause': 'Pysäytyskuva', 'Panning': 'Panorointi',
    'Slow-motion': 'Hidastus', 'Ambience': 'Ambienssi', 'Silence': 'Hiljaisuus',
    'Session': 'Sessio', 'Theme': 'Teema', 'Location': 'Paikka', 'Voice': 'Istuva',
    'AI': 'Tekoäly', 'Emoji': 'Emojit', 'Title': 'Otsikko', 'System': 'Ohjeistus',
    'ai-system': 'Järjestelmäviesti.', 'ai-user1': 'Käyttäjän syöte #1',
    'ai-ai1': 'Tekoälyn vaste #1', 'ai-user2': 'Käyttäjän syöte #2',
    'ai-ai2': 'Tekoälyn vaste #2', 'Example1': 'Esim #1', 'Example2': 'Esim #2',
    'Dark': 'Tumma', 'Light': 'Vaalea', 'theme-wide': 'Laajakuva', 'theme-43': '4:3',
    'theme-landscape': 'Vaaka', 'theme-portrait': 'Pysty', 'Empty': 'Tyhjä',
    'Adjust': 'Säätö', 'Speech': 'Puhe', 'Silence': 'Hiljaisuus', 'Framing': 'Rajaus',
    'Mixer': 'Mikseri', 'Space': 'Tila', 'Dry': 'Suora', 'voice-test': 'Äänitesti',
    'Limits': 'Rajat', 'ai-stop': 'Stop', 'ai-stopword': 'Avainsana', 'ai-user': 'Käyttäjä',
    'ai-username': 'Nimi', 'input': 'Kirjoita viesti.', 'Name': 'Nimi', 'Language': 'Kieli',
    'en': 'English', 'fi': 'Finnish', 'words': 'sanaa', 'dialogs': 'sanomaa',
    'Manuscript': 'Käsikirjoitus', 'Exclude': 'Ohita', 'Italics': 'Kursiivi',
    'Code': 'Koodi', 'Light': 'Valo', 'LightAmbient': 'Ambientti',
    'LightDirect': 'Suunnattu', 'LightSpot': 'Spotti', 'theme-full': "Täysi",
    'lt': 'Liettua', 'test-sentence': "Kirjoita tähän testilause.", 'Mid': 'Keskiosa',
    'Gesture': 'Ele', 'PoseMovement': 'Liike', 'Visible': 'Näytä',
    'Helper': 'Apuri', 'Edit': 'Muokkaa', 'Script': 'Skripti', 'fr': "Ranska",
    'de': "Saksa"
  },

  'en': {
    'ai-system': 'System message.', 'ai-user1': 'User example #1',
    'ai-ai1': 'AI response #1', 'ai-user2': 'User example #2',
    'ai-ai2': 'AI response #2', 'Example1': 'Example-1', 'Example2': 'Example-2',
    'theme-wide': 'Widescreen', 'theme-43': '4:3', 'theme-landscape': 'Landscape',
    'theme-portrait': 'Portrait', 'voice-test': 'Speak', 'ai-stop': 'Stop',
    'ai-user': 'User', 'input': 'Message.', 'en': 'English', 'fi': 'Finnish',
    'ai-stopword': 'Word', 'ai-username': 'Name', 'LightAmbient': 'Ambient',
    'LightDirect': 'Direct', 'LightSpot': 'Spot', 'theme-full': "Fullscreen",
    'lt': 'Lithuanian', 'test-sentence': "Write your test sentence.",
    'PoseMovement': 'Movement', 'fr': "French", 'de': "German"
  }

}

// i18n
// Default UI language is English

function i18nWord(w,l) {
  l = l || cfg('theme-lang') || 'en';
  return (( i18n[l] && i18n[l][w] ) ? i18n[l][w] : w);
}

function i18nTranslate(l) {
  l = l || cfg('theme-lang') || 'en';

  // Text
  d3.selectAll("[data-i18n-text]").nodes().forEach( n => {
    const e = d3.select(n);
    e.text( i18nWord(e.attr("data-i18n-text"),l ) );
  });

  // Title
  d3.selectAll("[data-i18n-title]").nodes().forEach( n => {
    const e = d3.select(n);
    e.attr( 'title', i18nWord( e.attr("data-i18n-title"),l ) );
  });

  // Placeholder
  d3.selectAll("[data-i18n-placeholder]").nodes().forEach( n => {
    const e = d3.select(n);
    e.attr( 'placeholder', i18nWord( e.attr("data-i18n-placeholder"),l) );
  });

  // Site
  d3.selectAll("[data-i18n-site]").nodes().forEach( n => {
    const e = d3.select(n);
    const label = e.attr("data-i18n-site");
    const [section, ...rest] = label.split('-');
    const item = rest.join('-');
    let text = item;
    if ( site[section] && site[section][item] && site[section][item][l] ) {
      text = site[section][item][l];
    }
    e.text( text );
  });
}

// Markdown configuration
const markedOptions = { gfm: true, breaks: true };

// Open AI configuration
let aiController = null;

// ElevenLabs configuration
const elevenBOS = {
  "text": " ",
  "voice_settings": { "stability": 0.8, "similarity_boost": true },
  "generation_config": {
    "chunk_length_schedule": [500,500,500,500]
  }
};
let elevenSocket = null;
let elevenInputMsgs = null;
let elevenOutputMsg = null;
let elevenOnProcessed = null;

// JSON Web Token (JWT)
let jwtExpires = 0;
let jwt = '';

// Get JSON Web Token
async function jwtGet() {
  const limit = Math.round(Date.now() / 1000) + 60;
  if ( jwtExpires < limit ) {
    try {
      const o = await (await fetch( jwtEndpoint, { cache: "no-store" } )).json();
      if ( o && o.jwt ) {
        const b64Url = o.jwt.split('.')[1];
        const b64 = b64Url.replace(/-/g, '+').replace(/_/g, '/');
        const s = decodeURIComponent( window.atob(b64).split('').map( (c) => {
          return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
        const p = JSON.parse(s);
        jwtExpires = (p && p.exp) ? p.exp : 0;
        jwt = o.jwt;
      } else {
        jwt = '';
        jwtExpires = 0;
      }
    } catch(e) {
      console.error(e);
      jwt = '';
      jwtExpires = 0;
    }
  }
  return jwt.slice();
}

// Speak using ElevenLabs
async function elevenSpeak(s,node=null) {
  if ( !elevenSocket ) {
    // Temporary reservation of WebSocket connection
    elevenSocket = { readyState: 0 };

    // Temporary stack of message until the connection is established
    elevenInputMsgs = [
      elevenBOS,
      {
        "text": s,
        "try_trigger_generation": false,
        "flush": true
      }
    ];

    // Endpoint/proxy and authentication
    let url;
    const apikey = d3.select("#apikey-eleven").property("value");
    if ( apikey ) {
      url = elevenTTSEndpoint[0];
      url += cfg('voice-eleven-id');
      url += elevenTTSEndpoint[1];
      elevenInputMsgs[0]["xi_api_key"] = apikey;
    } else {
      url = elevenTTSProxy[0];
      url += await jwtGet();
      url += elevenTTSProxy[1];
      url += cfg('voice-eleven-id');
      url += elevenTTSProxy[2];
    }

    // Make the connection
    elevenSocket = new WebSocket(url);

    // Connection opened
    elevenSocket.onopen = function (event) {
      elevenOutputMsg = null;
      while (elevenInputMsgs.length > 0) {
        elevenSocket.send(JSON.stringify(elevenInputMsgs.shift()));
      }
    }

    // New message received
    elevenSocket.onmessage = function (event) {
      const r = JSON.parse(event.data);

      // Speak audio
      if ( (r.isFinal || r.normalizedAlignment) && elevenOutputMsg ) {
        head.speakAudio( elevenOutputMsg, { lipsyncLang: cfg('voice-lipsync-lang') }, node ? addText.bind(null,node) : null );
        if ( elevenOnProcessed ) {
          elevenOnProcessed();
        }
        elevenOutputMsg = null;
      }

      if ( !r.isFinal ) {
        // New part
        if ( r.alignment ) {
          elevenOutputMsg = { audio: [], words: [], wtimes: [], wdurations: [] };

          // Parse chars to words
          let word = '';
          let time = 0;
          let duration = 0;
          for( let i=0; i<r.alignment.chars.length; i++ ) {
            if ( word.length === 0 ) time = r.alignment.charStartTimesMs[i];
            if ( word.length && r.alignment.chars[i] === ' ' ) {
              elevenOutputMsg.words.push( word );
              elevenOutputMsg.wtimes.push(time);
              elevenOutputMsg.wdurations.push(duration);
              word = '';
              duration = 0;
            } else {
              duration += r.alignment.charDurationsMs[i];
              word += r.alignment.chars[i];
            }
          }
          if ( word.length ) {
            elevenOutputMsg.words.push(word);
            elevenOutputMsg.wtimes.push(time);
            elevenOutputMsg.wdurations.push(duration);
          }
        }

        // Add audio content to message
        if ( r.audio &&  elevenOutputMsg ) {
          elevenOutputMsg.audio.push( head.b64ToArrayBuffer( r.audio ) );
        }
      }
    };

    // Error
    elevenSocket.onerror = function (error) {
      if ( elevenOnProcessed ) elevenOnProcessed();
      console.error(`WebSocket Error: ${error}`);
    };

    // Connection closed
    elevenSocket.onclose = function (event) {
      if ( elevenOnProcessed ) elevenOnProcessed();
      if ( event.wasClean) {
        // console.info(`Connection closed cleanly, code=${event.code}, reason=${event.reason}`);
      } else {
        console.warn('Connection died');
      }
      elevenSocket = null;
    };
  } else {
    let msg = {
      "text": s
    };
    if ( s.length ) {
      msg["try_trigger_generation"] = false;
      msg["flush"] = true;
    }
    if (elevenSocket.readyState === 1) { // OPEN
      elevenSocket.send(JSON.stringify(msg))
    } else if ( elevenSocket.readyState === 0 ) { // CONNECTING
      elevenInputMsgs.push(msg);
    }
  }
}

// Speak using Microsoft
let microsoftSynthesizer = null;
const microsoftQueue= [];

async function microsoftSpeak(s, node=null, onprocessed=null) {

  if ( s === null ) {
    microsoftQueue.push( null );
  } else {

    // Voice config
    const id = cfg("voice-microsoft-id");
    const e = d3.select("[data-voice-microsoft-id='" + id + "']");
    const lang = e.attr("data-voice-microsoft-lang");

    // SSML
    const ssml = "<speak version='1.0' " +
      "xmlns:mstts='http://www.w3.org/2001/mstts' " +
      "xml:lang='" + lang + "'>" +
      "<voice name='" + id + "'>" +
      "<mstts:viseme type='redlips_front'/>" +
      s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;') +
      "</voice>" +
      "</speak>";

    microsoftQueue.push( {
      ssml: ssml,
      node: node,
      onprocessed: onprocessed,
      speak: {
        audio: [], words: [], wtimes: [], wdurations: [],
        visemes: [], vtimes: [], vdurations: [], markers: [], mtimes: []
      }
     } );
  }

  // If this was the first item, start the process
  if ( microsoftQueue.length === 1 ) {
    microsoftProcessQueue();
  }
}

async function microsoftProcessQueue() {

  if ( microsoftQueue.length ) {

    const job = microsoftQueue[0];

    if ( job === null ) {
      microsoftQueue.shift();
      if ( microsoftQueue.length === 0 &&  microsoftSynthesizer ) {
        microsoftSynthesizer.close();
        microsoftSynthesizer = null;
      }
    } else {

      // If we do not have a speech synthesizer, create a new
      if ( !microsoftSynthesizer ) {

        // Create a new speech synthesizer
        let endpoint;
        let config;
        const apikey = d3.select("#apikey-microsoft").property("value");
        if ( apikey ) {
          endpoint = microsoftTTSEndpoint;
          config = window.SpeechSDK.SpeechConfig.fromEndpoint(endpoint, apikey);
        } else {
          endpoint = microsoftTTSProxy[0] + await jwtGet() + microsoftTTSProxy[1];
          config = window.SpeechSDK.SpeechConfig.fromEndpoint(endpoint);
        }
        config.setProperty("SpeechServiceConnection_Endpoint",endpoint);
        config.speechSynthesisOutputFormat = window.SpeechSDK.SpeechSynthesisOutputFormat.Raw22050Hz16BitMonoPcm;
        config.setProperty(window.SpeechSDK.PropertyId.SpeechServiceResponse_RequestSentenceBoundary, "true");
        microsoftSynthesizer = new window.SpeechSDK.SpeechSynthesizer(config,null);

        // Viseme conversion from Microsoft to Oculus
        // TODO: Check this conversion again!
        const visemeMap = [
          "sil", 'aa', 'aa', 'O', 'E', // 0 - 4
          'E', 'I', 'U', 'O', 'aa', // 5 - 9
          'O', 'I', 'kk', 'RR', 'nn', // 10 - 14
          'SS', 'SS', 'TH', 'FF', 'DD', // 15 - 19
          'kk', 'PP' // 20 - 21
        ];

        // Process visemes
        microsoftSynthesizer.visemeReceived = function(s, e) {
          if ( microsoftQueue[0] && microsoftQueue[0].speak ) {
            const o = microsoftQueue[0].speak;
            const viseme = visemeMap[e.visemeId];
            const time = e.audioOffset / 10000;

            // Calculate the duration of the previous viseme
            if ( o.vdurations.length ) {

              if ( o.visemes[ o.visemes.length-1 ] === 0 ) {
                o.visemes.pop();
                o.vtimes.pop();
                o.vdurations.pop();
              } else {
                // Remove silence
                o.vdurations[ o.vdurations.length-1 ] = time - o.vtimes[ o.vdurations.length-1 ];
              }
            }



            // Add this viseme
            o.visemes.push( viseme );
            o.vtimes.push( time );
            o.vdurations.push( 75 ); // Duration will be fixed when the next viseme is received
          }
        };

        // Process word boundaries and punctuations
        microsoftSynthesizer.wordBoundary = function (s, e) {
          if ( microsoftQueue[0] && microsoftQueue[0].speak ) {
            const o = microsoftQueue[0].speak;
            const word = e.text;
            const time = e.audioOffset / 10000;
            const duration = e.duration / 10000;

            if ( e.boundaryType === "PunctuationBoundary" && o.words.length ) {
              o.words[ o.words.length-1 ] += word;
            } else if ( e.boundaryType === "WordBoundary" || e.boundaryType === "PunctuationBoundary" ) {
              o.words.push( word );
              o.wtimes.push( time );
              o.wdurations.push( duration );
            } else if ( e.boundaryType === "SentenceBoundary" ) {
              if ( time > 500 ) {
                o.markers.push( () => { head.lookAtCamera(500); });
                o.mtimes.push( time - 500 );
              }
            }
          }
        };

      }

      // Speak the SSML
      microsoftSynthesizer.speakSsmlAsync(job.ssml,
        function (result) {
          if ( microsoftQueue[0] && microsoftQueue[0].speak ) {
            if (result.reason === window.SpeechSDK.ResultReason.SynthesizingAudioCompleted) {
              const job = microsoftQueue[0];
              job.speak.audio.push( result.audioData );
              head.speakAudio(job.speak, {}, job.node ? addText.bind(null,job.node) : null );
              if ( job.onprocessed ) job.onprocessed();
            }
            microsoftQueue.shift();
            microsoftProcessQueue();
          }
        }, function (err) {
          if ( job.onprocessed ) job.onprocessed();
          console.log(err);
          microsoftQueue.shift();
          microsoftProcessQueue();
        }
      );

    }
  }
}

// Whisper MP3
let whisperAudio = null;
let whisperLipsyncLang = 'en';

async function whisperLoadMP3(file) {

  if ( cfg("whisper-type") === "openai" ) {
    try {
      d3.select("#playmp3").classed("disabled", true);

      const form = new FormData();
      form.append("file", file);
      form.append("model", "whisper-1");
      form.append("response_format", "verbose_json" );
      form.append("prompt","[The following is a full verbatim transcription without additional details, comments or emojis:]");
      form.append("timestamp_granularities[]", "word" );
      form.append("timestamp_granularities[]", "segment" );

      // Endpoint/proxy and authentication
      let url;
      let headers = {};
      const apikey = d3.select("#apikey-openai").property("value");
      if ( apikey ) {
        url = openaiAudioTranscriptionsEndpoint;
        headers["Authorization"] = "Bearer " + apikey;
      } else {
        url = openaiAudioTranscriptionsProxy;
        headers["Authorization"] = "Bearer " + await jwtGet()
      }

      const response = await fetch( url , {
        method: "POST",
        body: form,
        headers: headers
      });
      if ( response.ok ) {
        const json = await response.json();
        d3.select("#jsonmp3").property("value", JSON.stringify(json) );

        // Fetch audio
        if ( json.words && json.words.length ) {
          var reader = new FileReader();
          reader.readAsArrayBuffer(file);
          reader.onload = async readerEvent => {
            let arraybuffer = readerEvent.target.result;
            let audiobuffer = await head.audioCtx.decodeAudioData(arraybuffer);

            // Set lip-sync language
            whisperLipsyncLang = json.language.substring(0,2);

            // Add words to the audio object
            whisperAudio = {
              audio: audiobuffer,
              words: [],
              wtimes: [],
              wdurations: [],
              markers: [],
              mtimes: []
            };
            json.words.forEach( x => {
              // Word
              whisperAudio.words.push( x.word );

              // Starting time
              let t = 1000 * x.start;
              if ( t > 150 ) t -= 150;
              whisperAudio.wtimes.push( t );

              // Duration
              let d = 1000 * (x.end - x.start);
              if ( d > 20 ) d -= 20;
              whisperAudio.wdurations.push( d  );
            });

            // Add timed callback markers to the audio object
            const startSegment = async () => {
              // Look at the camera
              head.lookAtCamera(500);
              head.speakWithHands();
            };
            json.segments.forEach( x => {
              if ( x.start > 2 && x.text.length > 10 ) {
                whisperAudio.markers.push( startSegment );
                whisperAudio.mtimes.push( 1000 * x.start - 1000 );
              }
            });

            d3.select("#playmp3").classed("disabled", false);
          }
        }

      } else {
        d3.select("#jsonmp3").property("value", 'Error: ' + response.status + ' ' + response.statusText );
        console.log(response);
      }

    } catch (error) {
      console.log(error);
    }
  } else if ( cfg("whisper-type") === "local" ) {
    try {
      d3.select("#playmp3").classed("disabled", true);

      const form = new FormData();
      form.append("file", file);
      form.append("temperature","0.0");
      form.append("temperature_inc", "0.2");
      form.append("response_format", "verbose_json");
      const response = await fetch( localWhisperCppProxy + 'inference' , {
        method: "POST",
        body: form,
        headers: {
          "Authorization": "Bearer " + await jwtGet()
        }
      });
      if ( response.ok ) {
        const json = await response.json();
        d3.select("#jsonmp3").property("value", JSON.stringify(json) );

        // Fetch audio
        if ( json.segments && json.segments.length ) {
          var reader = new FileReader();
          reader.readAsArrayBuffer(file);
          reader.onload = async readerEvent => {
            let arraybuffer = readerEvent.target.result;
            let audiobuffer = await head.audioCtx.decodeAudioData(arraybuffer);

            // Set lip-sync language
            whisperLipsyncLang = json.language.substring(0,2);

            // Add words to the audio object
            whisperAudio = {
              audio: audiobuffer,
              words: [],
              wtimes: [],
              wdurations: [],
              markers: [],
              mtimes: []
            };

            // Add timed callback markers to the audio object
            const startSegment = async () => {
              // Look at the camera
              head.lookAtCamera(500);
              head.speakWithHands();
            };

            json.segments.forEach( (x,i) => {
              if ( x.words && x.words.length ) {

                // Look at camera
                if ( x.start > 2 && x.text.length > 10 ) {
                  whisperAudio.markers.push( startSegment );
                  whisperAudio.mtimes.push( 1000 * x.start - 1000 );
                }

                // Word-level timestamps
                x.words.forEach( y => {

                  // Word
                  whisperAudio.words.push( y.word );

                  // Starting time
                  let t = 1000 * y.start;
                  if ( t > 150 ) t -= 150;
                  whisperAudio.wtimes.push( t );

                  // Duration
                  let d = 1000 * (y.end - y.start);
                  if ( d <= 50 ) d = 50;
                  if ( d > 20 ) d -= 20;
                  whisperAudio.wdurations.push( d  );

                });
              }
            });

            d3.select("#playmp3").classed("disabled", false);
          }
        }

      } else {
        d3.select("#jsonmp3").property("value", 'Error: ' + response.status + ' ' + response.statusText );
        console.log(response);
      }

    } catch (error) {
      console.log(error);
    }
  }

}


// RECORDING
let recordingMediaRecorder = null;
let recordingAnalyzer = null;
let recordingIgnoreData = false;
let recordingChunks = [];
const recordingMediaTypes = [
  { type: "audio/webm", ext: "webm" },
  { type: "video/mp4", ext: "mp4" }
];
let recordingMediaType = {};
for( let i=0; i<recordingMediaTypes.length; i++ ) {
  if ( MediaRecorder.isTypeSupported(recordingMediaTypes[i].type) ) {
    recordingMediaType = recordingMediaTypes[i];
    break;
  }
}
const recordingBeep = "data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+ Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ 0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7 FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb//////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=";
const recordingPlaySound = (function beep() {
  const snd = new Audio(recordingBeep);
  return function() { snd.play(); }
})();


async function recordingRecord() {
  if ( !recordingMediaRecorder ) {
    try {
      if ( !navigator.mediaDevices ) throw new Error("No navigator.mediaDevices available for recording.");
      let stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
      recordingMediaRecorder = new MediaRecorder(stream, { mimeType: recordingMediaType.type });
      recordingMediaRecorder.ondataavailable = async (event) => {

        // If marked ignore, do not transcribe
        if ( recordingIgnoreData ) {
          recordingIgnoreData = false;
          return;
        } else {
          console.log('Recorded data:',event);
          recordingIgnoreData = false;
        }

        if ( event.data.size > 0 ) {

          // Make a transcription
          const file = new File([event.data], 'file.'+recordingMediaType.ext, { type: recordingMediaType.type });
          const form = new FormData();
          form.append("file", file);

          if ( cfg("whisper-type") === "openai" ) {

            form.append("model", "whisper-1");
            const lang = cfg('theme-lang') || 'en';
            form.append("language", lang);
            const response = await fetch( openaiAudioTranscriptionsProxy , {
              method: "POST",
              body: form,
              headers: {
                "Authorization": "Bearer " + await jwtGet()
              }
            });

            // Append text to input
            if ( response.ok ) {
              let json;
              try {
                json = await response.json();
              } catch(error) {
                console.error("Invalid response from OpenAI");
              }
              if ( json && json.text ) {
                const e = d3.select("#input");
                let text = e.property("value");
                if ( text ) {
                  e.property("value", text + ' ' + json.text );
                } else {
                  e.property("value", json.text );
                }
              }
            }
          } else if ( cfg("whisper-type") === "local" ) {

            form.append("temperature","0.0");
            form.append("temperature_inc", "0.2");
            form.append("response_format", "json");

            const response = await fetch( localWhisperCppProxy + 'inference' , {
              method: "POST",
              body: form,
              headers: {
                "Authorization": "Bearer " + await jwtGet()
              }
            });

            // Append text to input
            if ( response.ok ) {
              let json;
              try {
                json = await response.json();
              } catch(error) {
                console.error("Invalid response from local Whisper");
              }
              if ( json && json.text ) {
                const e = d3.select("#input");
                let text = e.property("value");

                let s = json.text
                  .replace(/\[[^\]]+\]/g, '')
                  .replaceAll(/[^\p{L}\p{N},\.\p{Quotation_Mark}!€\$\+\p{Dash_Punctuation}%&\?\(\)\* ]/ug,"")
                  .replaceAll("  "," ")
                  .trim();

                if ( text ) {
                  e.property("value", text + ' ' + s );
                } else {
                  e.property("value", s );
                }
              }
            }

          }

        }
      };

      // Connect to TalkingHead
      recordingAnalyzer = head.audioCtx.createAnalyser();
      const recordingSource = head.audioCtx.createMediaStreamSource(stream);
      recordingSource.connect(recordingAnalyzer);

    } catch (error) {
      d3.select("#record").classed("selected",false);
      console.error('Error accessing microphone:', error);
      return;
    }
  }

  try {
    if ( recordingAnalyzer ) {
      head.startListening( recordingAnalyzer,{},(status) => {

        switch(status) {
        case 'start':
          head.lookAtCamera(2000);
          break;
        case 'stop':
          if ( recordingMediaRecorder ) {
            // recordingMediaRecorder.requestData();
            recordingMediaRecorder.stop();
            recordingMediaRecorder.start();
          }
          break;
        case 'maxsilence':
          if ( recordingMediaRecorder ) {
            recordingIgnoreData = true;
            // recordingMediaRecorder.requestData();
            recordingMediaRecorder.stop();
            recordingMediaRecorder.start();
          }
          break;
        case 'maxactive':
          if ( recordingMediaRecorder ) {
            recordingIgnoreData = true;
            // recordingMediaRecorder.requestData();
            recordingMediaRecorder.stop();
            recordingMediaRecorder.start();
          }
          break;
        }
      });
    }

    recordingIgnoreData = false;
    if ( recordingMediaRecorder ) {
      recordingMediaRecorder.start();
    }

  } catch(error) {
    d3.select("#record").classed("selected",false);
    console.error('Recorder error:', error);
  }
}


// Number of words on a string.
function nWords(str) {
  return str ? str.trim().split(/\s+/).length : 0;
}

// Default settings
let CFG = {
  session: 0,
  sessions : [
    {
      name: "Nimetön",
      theme: { lang: 'en', brightness:"dark", ratio:"wide", layout:"port" },
      view: { image: 'NONE' },
      avatar: {},
      camera: { frame: 'full' },
      ai: {},
      voice: { background: "NONE", type: "google", google:{ id: "en-GB-Standard-A"}, lipsync:{ lang: 'en' } }
    },
    {
      name: "Nimetön 2",

      theme: { lang: 'en', brightness: "dark", ratio: "wide", layout: "land" },
      view: { image: 'NONE' },
      avatar: {},
      camera: { frame: 'upper' },
      ai: {},
      voice: { background: "NONE", type: "google", google:{ id: "en-GB-Standard-A"}, lipsync:{ lang: 'en' } }
    },
  ]
};

// Dynamically created icons
const svgSelect = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" /></svg>';
const svgSpeak = '<svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.939C14.2091 18.939 16 17.1482 16 14.939C16 12.7299 14.2091 10.939 12 10.939C9.79086 10.939 8 12.7299 8 14.939C8 17.1482 9.79086 18.939 12 18.939ZM12 16.939C13.1046 16.939 14 16.0436 14 14.939C14 13.8345 13.1046 12.939 12 12.939C10.8954 12.939 10 13.8345 10 14.939C10 16.0436 10.8954 16.939 12 16.939Z" fill="currentColor" /><path d="M12 9.04401C13.1046 9.04401 14 8.14858 14 7.04401C14 5.93944 13.1046 5.04401 12 5.04401C10.8954 5.04401 10 5.93944 10 7.04401C10 8.14858 10.8954 9.04401 12 9.04401Z" fill="currentColor" /><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C5.34315 1 4 2.34315 4 4V20C4 21.6569 5.34315 23 7 23H17C18.6569 23 20 21.6569 20 20V4C20 2.34315 18.6569 1 17 1H7ZM17 3H7C6.44772 3 6 3.44772 6 4V20C6 20.5523 6.44772 21 7 21H17C17.5523 21 18 20.5523 18 20V4C18 3.44772 17.5523 3 17 3Z" fill="currentColor" /></svg>'
const svgStop = '<svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg"><path d="M6.2253 4.81108C5.83477 4.42056 5.20161 4.42056 4.81108 4.81108C4.42056 5.20161 4.42056 5.83477 4.81108 6.2253L10.5858 12L4.81114 17.7747C4.42062 18.1652 4.42062 18.7984 4.81114 19.1889C5.20167 19.5794 5.83483 19.5794 6.22535 19.1889L12 13.4142L17.7747 19.1889C18.1652 19.5794 18.7984 19.5794 19.1889 19.1889C19.5794 18.7984 19.5794 18.1652 19.1889 17.7747L13.4142 12L19.189 6.2253C19.5795 5.83477 19.5795 5.20161 19.189 4.81108C18.7985 4.42056 18.1653 4.42056 17.7748 4.81108L12 10.5858L6.2253 4.81108Z" fill="currentColor" /></svg>';
const svgRepost = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.1459 11.0499L12.9716 9.05752L15.3462 8.84977C14.4471 7.98322 13.2242 7.4503 11.8769 7.4503C9.11547 7.4503 6.87689 9.68888 6.87689 12.4503C6.87689 15.2117 9.11547 17.4503 11.8769 17.4503C13.6977 17.4503 15.2911 16.4771 16.1654 15.0224L18.1682 15.5231C17.0301 17.8487 14.6405 19.4503 11.8769 19.4503C8.0109 19.4503 4.87689 16.3163 4.87689 12.4503C4.87689 8.58431 8.0109 5.4503 11.8769 5.4503C13.8233 5.4503 15.5842 6.24474 16.853 7.52706L16.6078 4.72412L18.6002 4.5498L19.1231 10.527L13.1459 11.0499Z" fill="currentColor" /></svg>';

// Get/set config value for the current session
function cfg(key,value) {
  if ( key === undefined ) return CFG.sessions[CFG.session];
  let parts = key.split('-').map( x => isNaN(x) ? x : parseInt(x) );
  if ( value === undefined ) {
    return parts.reduce( (o,p) => o ? o[p] : undefined, CFG.sessions[CFG.session] );
  } else {
    parts.reduce( (o,p,i) => {
      const def = (typeof parts[i+1] === 'number') ? [] : {};
      return (o[p] = (parts.length-1) === i ? value : (o[p] || def));
    }, CFG.sessions[CFG.session] );
  }
}

// Load settings
try {
  let json = localStorage.getItem('talkinghead');
  CFG = (json ? JSON.parse(json) : CFG);
} catch(error) {
  alert("Invalid JSON settings");
  console.error(error);
}

window.cfg = cfg;

// Save config to localStorage and show active session config
let loadingConfig = false;
function saveConfig() {
  if ( !loadingConfig ) {
    let json = JSON.stringify(CFG);
    localStorage.setItem("talkinghead", json);
    json = JSON.stringify( cfg() );
    d3.select("#json").property("value", json );
  }
}

// Load config for the given session
function loadConfig(session = null) {
  try {
    loadingConfig = true;
    let json = localStorage.getItem('talkinghead');
    const config = (json ? JSON.parse(json) : CFG);
    if ( config && config.hasOwnProperty("session") && config.hasOwnProperty("sessions") && config.sessions.length ) {
      CFG = config;
      if ( session !== null && session >= 0 && session < CFG.sessions.length ) {
        CFG.session = session;
        json = JSON.stringify(CFG);
        localStorage.setItem("talkinghead", json);
      }

      // Populate sessions
      d3.selectAll(".session").nodes().forEach( n => {
        const e = d3.select(n);
        const id = parseInt( e.property("id").split("-")[1] );
        if ( id >= 0 && id < CFG.sessions.length ) {
          e.classed("selected", (id === CFG.session) );
        } else {
          e.remove();
        }
      });
      for( let i=0; i<CFG.sessions.length; i++ ) {
        const e = d3.select("#session-"+i);
        if ( e.empty() ) {
          d3.select("#sessions").append("div")
            .property("id","session-"+i)
            .classed("session", true)
            .classed("selected", (i === CFG.session) );
        }
      }

      // Populate directory
      d3.selectAll(".entry").nodes().forEach( n => {
        const e = d3.select(n);
        const id = parseInt( e.property("id").split("-")[1] );
        if ( id >= 0 && id < CFG.sessions.length ) {
          let name = CFG.sessions[id].name;
          if ( !name || name.length === 0 ) {
            name = "Nimetön";
            CFG.sessions[id].name = name;
          }
          e.select("div")
            .classed("selected", (id === CFG.session) )
            .text( name );
        } else {
          e.remove();
        }
      });
      for( let i=0; i<CFG.sessions.length; i++ ) {
        const e = d3.select("#entry-"+i);
        if ( e.empty() ) {
          let n = d3.select("#directory").node().lastElementChild;
          let name = CFG.sessions[i].name;
          if ( !name || name.length === 0 ) {
            name = "Nimetön";
            CFG.sessions[i].name = name;
          }
          let clone = d3.select(n).clone(true);
          clone.property("id","entry-"+i);
          clone.select("[data-session]")
            .attr("data-session", i)
            .classed("selected", (i === CFG.session) )
            .text(name)
            .on('click.command', entrySelect);
          clone.selectAll("[data-entry-move]")
            .on('click.command', entryMove );
        }
      }

      // Populate settings page in specific order
      [
        "[data-item='view-url']", "[data-item='avatar-url']",
        "[data-item='avatar-body']","[data-item]"
      ].forEach( x => {
        d3.selectAll(x).nodes().forEach( (n) => {
          const e = d3.select(n);
          const item = e.attr("data-item");
          const type = e.attr("data-type");
          const range = e.attr("data-range");
          let value = cfg( item );
          if ( value !== undefined ) {
            if ( type === 'boolean' ) {
              e.classed( "selected", value )
            } else if ( type === 'option' ) {
              if ( value === e.attr("data-"+item) ) {
                e.dispatch("click");
              }
            } else if ( type === 'value' ) {
              e.property("value",value).dispatch("change");
            }
          } else {
            if ( type === 'boolean' ) {
              cfg( item, e.classed( "selected" ) );
            } else if ( type === 'option' ) {
              if ( e.classed("selected") ) {
                cfg( item, e.attr("data-"+item) );
                e.dispatch("click");
              }
            } else {
              if ( range !== null ) {
                cfg( item, parseFloat( e.property("value") ) );
              } else {
                cfg( item, e.property("value") );
              }
              e.dispatch("change");
            }
          }
        });
      });

      // Populate other parts of UI,
      d3.select("#name").text( cfg("name") );
      if ( d3.select("[data-item='view-image'].selected").empty() ) {
        d3.select("[data-item='view-image']").classed("selected",true).dispatch('click');
      }
      if ( d3.select("[data-item='avatar-name'].selected").empty() ) {
        d3.select("[data-item='avatar-name']").classed("selected",true).dispatch('click');
      }
      scriptInit();
      json = JSON.stringify( cfg() );
      d3.select("#json").property("value",json);
    }
  } catch(error) {
    alert("Invalid JSON settings");
    console.error(error);
  } finally {
    loadingConfig = false;
    saveConfig();
  }
}

// Process string s for parts to exclude from speech/lip-sync
// For continued stream of strings the previous state o can be given
function excludesProcess(s,o=null) {

  // If no previous rules and states, build rules based on user settings
  if ( !o || !o.rules || !Array.isArray(o.rules) ) {
    o = { rules: [] };
    if ( cfg('voice-exclude-italics') ) {
      o.rules.push( { separator: '*', open: false });
    }
    if ( cfg('voice-exclude-code') ) {
      o.rules.push( { separator: '```', open: false });
    }
  }

  // Excludes is an array of [start,end] index pairs
  o.excludes = [];

  // If there are rules, process them
  o.rules.forEach( x => {
    const parts = s.split(x.separator);
    let i = 0;
    parts.forEach( (y,j) => {
      const isLast = (j === (parts.length - 1));
      if ( x.open ) {
        const start = i - (j===0 ? 0 : x.separator.length);
        const end = i + y.length - 1 + (isLast ? x.separator.length : 0);
        o.excludes.push( [start, end] ); // Exclude
      }
      if ( !isLast ) {
        i += y.length + x.separator.length;
        x.open = !x.open;
      }
    });
  });

  return o;
}

function motion( action, pose, gesture, mood ) {
  try {
    head.setMood(mood || 'neutral');
  } catch(err) {}
  if ( gesture && site.gestures[gesture] ) {
    head.playGesture(site.gestures[gesture].name);
  }
  if ( action && site.animations[action] ) {
    head.playAnimation( site.animations[action].url, progressUpdate, site.animations[action].dur || 20 );
  } else if ( pose && site.poses[pose] ) {
    head.playPose( site.poses[pose].url, progressUpdate, site.poses[pose].dur || 60 );
  }
}

// Build outgoing message for OpenAI
function openaiBuildMessage() {
  const msgs = [];

  const systems = [
    { sel: "[data-ai-openai-system]", role: "system" },
    { sel: "[data-ai-openai-user1]", role: "user" },
    { sel: "[data-ai-openai-ai1]", role: "assistant" },
    { sel: "[data-ai-openai-user2]", role: "user" },
    { sel: "[data-ai-openai-ai2]", role: "assistant" }
  ];
  const session = d3.select(".session.selected");
  const input = d3.select("#input");
  const messages = session.selectAll(".message:not(.grayed)");

  const limitDialog = cfg('ai-openai-dialog');
  const limitInput = cfg('ai-openai-input');
  let dialogs = 0;
  let words = 0;

  // System messages
  systems.forEach( x => {
    const n = d3.select(x.sel).node();
    if ( n.value && n.value.length ) {
      if ( n.dataset.words ) {
        words += parseInt( n.dataset.words );
      } else {
        let wc = nWords( n.value );
        n.dataset.words = wc;
        words += wc;
      }
      msgs.push( { n: n, role: x.role, content: n.value } );
    }
  });

  // messages in reverse order
  const revmsgs = [];
  revmsgs.push( { n: input.node(), role: "user", content: input.property("value") } );
  words += nWords( input.property("value") );
  messages.nodes().reverse().forEach( (n) => {
    if ( dialogs < limitDialog && words < limitInput ) {
      let role;
      let val;
      if ( n.dataset.input && n.dataset.input.length ) {
        role = "user";
        val = n.dataset.input;
      } else if ( n.dataset.output && n.dataset.output.length ) {
        role = "assistant";
        val = n.dataset.output;
      }
      if ( role && val ) {
        revmsgs.push( { n: n, role: role, content: val } );
        if ( n.dataset.words ) {
          words += parseInt( n.dataset.words );
        } else {
          let wc = nWords( val );
          n.dataset.words = wc;
          words += wc;
        }
        dialogs++;
      }
    }
  });

  // Build message
  msgs.push( ...revmsgs.reverse() );

  return msgs;
}

// Moderate messages
async function openaiModerateMessage(msgs) {
  // Known status
  let flag = msgs.reduce( (a,b) => ((b.n && b.n.dataset.flag !== undefined) ? ( a | (b.n.dataset.flag === 'true') ) : a ), false);
  let score = msgs.reduce( (a,b) => ((b.n && b.n.dataset.score !== undefined) ? Math.max( a, parseFloat(b.n.dataset.score) ) : a ), 0.0);

  // Moderate only yet unmoderated messages, add the grouped message
  const modMsgs = msgs.filter( x => ( x.n && (x.n.dataset.flag === undefined) && x.content && x.content.length ) );
  if ( modMsgs.length === 0 ) return false;
  if ( msgs.length > 1 ) {
    const full = msgs.map( x => x.content ).join('\n\n');
    modMsgs.push( { content: full } );
  }

  // Endpoint/proxy and authentication
  let url;
  let headers = { "Content-Type": "application/json" };
  const apikey = d3.select("#apikey-openai").property("value");
  if ( apikey ) {
    url = openaiModerationsEndpoint;
    headers["Authorization"] = "Bearer " + apikey;
  } else {
    url = openaiModerationsProxy;
    headers["Authorization"] = "Bearer " + await jwtGet()
  }

  const text = modMsgs.map( x => x.content );
  try {
    const res = await fetch( url, {
      method: "POST",
      mode: "cors",
      headers: headers,
      body: JSON.stringify({
        input: text
      })
    });

    if ( res.ok ) {

      const data = await res.json();

      if ( data && data.results && data.results.length ) {
        data.results.forEach( (x,i) => {
          let flagtmp = Boolean( x.flagged );
          let scoretmp = 0.0;
          Object.keys(x.categories).forEach( key => {
            scoretmp = Math.max( scoretmp, x.category_scores[key] );
          });

          if ( modMsgs[i].n ) {
            modMsgs[i].n.dataset.flag = flagtmp;
            modMsgs[i].n.dataset.score = scoretmp;
          }

          flag |= flagtmp;
          score = Math.max(score,scoretmp);
        });
      }

      d3.select("#flag").classed("selected",flag);
      d3.select("#score").text( "" + Math.round(100 * score) / 100 );
    } else {
      console.error("Error: Moderation API returned ", res.status);
    }

  } catch (error) {
    console.error("Error: Moderation API returned ", error);
  }

  return flag;
}

// Send messages to OpenAI API and handle streamed response
async function openaiSendMessage(node,msgs) {

  // Create a new AbortController instance
  aiController = new AbortController();
  const signal = aiController.signal;

  // Endpoint/proxy and authentication
  let url;
  let headers = { "Content-Type": "application/json" };
  const apikey = d3.select("#apikey-openai").property("value");
  if ( apikey ) {
    url = openaiChatCompletionsEndpoint;
    headers["Authorization"] = "Bearer " + apikey;
  } else {
    url = openaiChatCompletionsProxy;
    headers["Authorization"] = "Bearer " + await jwtGet()
  }

  // Chat completion
  try {
    // Message body
    const body = {
      model: cfg('ai-model'),
      messages: msgs.map( x => {
        const {role,content,name} = x;
        return (name ? {role,content,name} : {role,content});
      }),
      temperature: cfg('ai-openai-temperature'),
      presence_penalty: cfg('ai-openai-presence'),
      frequency_penalty: cfg('ai-openai-frequency'),
      max_tokens: cfg('ai-openai-output'),
      stream: true
    };
    const stop = cfg('ai-openai-stop');
    if ( stop && stop.length ) {
      body.stop = [ stop ];
    }
    const user = cfg('ai-openai-user');
    if ( user && user.length ) {
      body.user = user;
    }
    const isMotionSelected = d3.select("#motion").classed("selected");
    if ( isMotionSelected ) {
      body.tools = [
        {
          "type": "function",
          "function": {
            "name": "move_body",
            "description": "Set the action, still pose and/or mood of your avatar's body in virtual world",
            "parameters": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "enum": Object.keys(site.animations)
                },
                "stillpose": {
                  "type": "string",
                  "enum": Object.keys(site.poses)
                },
                "gesture": {
                  "type": "string",
                  "enum": Object.keys(site.gestures)
                },
                "mood": {
                  "type": "string",
                  "enum": Object.keys(head.animMoods)
                }
              },
              "required": []
            }
          }
        }
      ];
      body.tool_choice = "auto";
      body.parallel_tool_calls = false;
    }

    // Elements
    node.dataset.output = '';
    let tts;
    let fn;

    do {

      // Ready for either text response or a function call
      tts = '';
      fn = { id: '', name: '', arguments: '' };

      // Fetch the response from the OpenAI API
      const res = await fetch( url, {
        method: "POST",
        mode: "cors",
        headers: headers,
        body: JSON.stringify(body),
        signal
      });

      if ( res.ok ) {

        // Read the response as a stream of data
        const reader = res.body.getReader();
        const decoder = new TextDecoder("utf-8");
        let exclude = null; // Exclude object

        while (true) {
          let { done, value } = await reader.read();

          // Extract text data
          if ( value ) {
            decoder.decode(value).split('\n').forEach( data => {
              if ( data.startsWith("data: ") ) {
                let line = data.substring(6).trim();
                if ( line === '[DONE]' ) {
                  done = true;
                } else {
                  let obj;
                  try {
                    obj = JSON.parse( line );
                  } catch(error) {
                    throw new Error( "Error JSON parsing line: " + line );
                  }

                  if ( obj && obj.error ) throw new Error( obj.error.message || ''+res.status );
                  fn.id += obj?.choices?.[0]?.delta?.tool_calls?.[0]?.id || '';
                  fn.name += obj?.choices?.[0]?.delta?.tool_calls?.[0]?.function?.name || '';
                  fn.arguments += obj?.choices?.[0]?.delta?.tool_calls?.[0]?.function?.arguments || '';
                  node.dataset.output += obj?.choices?.[0]?.delta?.content || '';
                  tts += obj?.choices?.[0]?.delta?.content || '';

                }
              }
            });
          }

          // Break, if done
          if (done) {
            if ( fn.id ) {

              // Function call
              body.messages.push({
                role: "assistant",
                content: null,
                tool_calls: [
                  {
                    id: fn.id,
                    type: "function",
                    function: {
                      name: fn.name,
                      arguments: fn.arguments
                    }
                  }
                ]
              });

              // Call the function
              try {
                const m = JSON.parse(fn.arguments);
                motion(m.action, m.stillpose, m.gesture, m.mood);
              } catch(motionError) {
                console.error(motionError);
              }

              // Response
              body.messages.push({
                role: "tool",
                content: "",
                tool_call_id: fn.id
              });

              // Make sure we do not get another function call
              body.tool_choice = "none";
            }

            if ( tts ) {
              if ( cfg('voice-type') === 'eleven' ) {
                await elevenSpeak( tts.trimStart() + " ", node );
              } else if ( cfg('voice-type') === 'microsoft' ) {
                await microsoftSpeak( tts.trimStart() + " ", node );
              } else {
                exclude = excludesProcess(tts,exclude);
                await head.speakText( tts, {
                  lipsyncLang: cfg('voice-lipsync-lang'),
                  ttsVoice: cfg('voice-google-id'),
                  ttsLang: cfg('voice-google-id').substring(0,5),
                  ttsRate: cfg('voice-google-rate'),
                  ttsPitch: cfg('voice-google-pitch')
                }, addText.bind(null,node), exclude.excludes );
              }
            }

            break;
          }

          // Speak
          if ( tts ) {
            let idx = 0;
            ['.','!','?','\n'].forEach( x => {
              let tmp = tts.lastIndexOf(x);
              if ( tmp > idx ) idx = tmp + 1;
            });
            if ( idx > 10 ) {
              let s = tts.substring(0, idx);
              if ( s && s.length ) {
                tts = tts.substring(idx).trimStart();
                if ( cfg('voice-type') === 'eleven' ) {
                  await elevenSpeak( s + " ", node );
                } else if ( cfg('voice-type') === 'microsoft' ) {
                  await microsoftSpeak( s + " ", node );
                } else {
                  exclude = excludesProcess(s,exclude);
                  await head.speakText( s, {
                    lipsyncLang: cfg('voice-lipsync-lang'),
                    ttsVoice: cfg('voice-google-id'),
                    ttsLang: cfg('voice-google-id').substring(0,5),
                    ttsRate: cfg('voice-google-rate'),
                    ttsPitch: cfg('voice-google-pitch')
                  }, addText.bind(null,node), exclude.excludes );
                }
              }
            }
          }

        }

        // Moderate
        let flag = await openaiModerateMessage([{ content: node.dataset.output, n: node }]);

      } else {
        console.error( await res.text() );
        throw new Error( json.error ? json.error.message : ''+res.status );
      }

    } while( fn.id ); // Repeat, if this iteration was a function call

  } catch (error) {
    if ( signal.aborted ) error = "aborted";
    console.error(error);
    addText( node, ' [' + error + ']');
  } finally {
    aiController = null; // Reset the AbortController instance
    if ( elevenSocket ) {
      elevenSpeak("",null);
    }
    microsoftSpeak(null);

    // When this marker has been reached, stop blinking
    head.speakMarker( () => {
      d3.selectAll('.blink').classed("blink",false);
    });
  }
}

// Grok-beta

// Send messages to local LLama API and handle streamed response
async function grokSendMessage(node,msgs) {

  // Create a new AbortController instance
  aiController = new AbortController();
  const signal = aiController.signal;

  // Endpoint/proxy and authentication
  let url;
  let headers = { "Content-Type": "application/json" };
  const apikey = d3.select("#apikey-grok").property("value");
  if ( apikey ) {
    url = grokChatCompletionsEndpoint;
    headers["Authorization"] = "Bearer " + apikey;
  } else {
    url = grokChatCompletionsProxy;
    headers["Authorization"] = "Bearer " + await jwtGet()
  }

  // Chat completion
  try {
    // Message body
    const body = {
      model: cfg('ai-model'),
      messages: msgs.map( x => {
        const {role,content,name} = x;
        return (name ? {role,content,name} : {role,content});
      }),
      temperature: cfg('ai-grok-temperature'),
      presence_penalty: cfg('ai-grok-presence'),
      frequency_penalty: cfg('ai-grok-frequency'),
      max_tokens: cfg('ai-grok-output'),
      stream: true
    };
    const stop = cfg('ai-grok-stop');
    if ( stop && stop.length ) {
      body.stop = [ stop ];
    }
    const user = cfg('ai-grok-user');
    if ( user && user.length ) {
      body.user = user;
    }

    // Elements
    node.dataset.output = '';
    let tts = '';

    // Fetch the response from the Llama.cpp server
    tts = '';
    const res = await fetch( url, {
      method: "POST",
      mode: "cors",
      headers: headers,
      body: JSON.stringify(body),
      signal
    });

    if ( res.ok ) {

      // Read the response as a stream of data
      const reader = res.body.getReader();
      const decoder = new TextDecoder("utf-8");
      let exclude = null; // Exclude object

      while (true) {
        let { done, value } = await reader.read();

        // Extract text data
        if ( value ) {
          decoder.decode(value).split('\n').forEach( data => {
            if ( data.startsWith("data: ") ) {
              let line = data.substring(6).trim();
              if ( line === '[DONE]' ) {
                done = true;
              } else {
                let obj;
                try {
                  obj = JSON.parse( line );
                } catch(error) {
                  throw new Error( "Error JSON parsing line: " + line );
                }

                if ( obj && obj.error ) throw new Error( obj.error.message || ''+res.status );
                node.dataset.output += obj?.choices?.[0]?.delta?.content || '';
                tts += obj?.choices?.[0]?.delta?.content || '';

              }
            }
          });
        }

        // Break, if done
        if (done) {


          if ( tts ) {
            if ( cfg('voice-type') === 'eleven' ) {
              await elevenSpeak( tts.trimStart() + " ", node );
            } else if ( cfg('voice-type') === 'microsoft' ) {
              await microsoftSpeak( tts.trimStart() + " ", node );
            } else {
              exclude = excludesProcess(tts,exclude);
              await head.speakText( tts, {
                lipsyncLang: cfg('voice-lipsync-lang'),
                ttsVoice: cfg('voice-google-id'),
                ttsLang: cfg('voice-google-id').substring(0,5),
                ttsRate: cfg('voice-google-rate'),
                ttsPitch: cfg('voice-google-pitch')
              }, addText.bind(null,node), exclude.excludes );
            }
          }

          break;
        }

        // Speak
        if ( tts ) {
          let idx = 0;
          ['.','!','?','\n'].forEach( x => {
            let tmp = tts.lastIndexOf(x);
            if ( tmp > idx ) idx = tmp + 1;
          });
          if ( idx > 10 ) {
            let s = tts.substring(0, idx);
            if ( s && s.length ) {
              tts = tts.substring(idx).trimStart();
              if ( cfg('voice-type') === 'eleven' ) {
                await elevenSpeak( s + " ", node );
              } else if ( cfg('voice-type') === 'microsoft' ) {
                await microsoftSpeak( s + " ", node );
              } else {
                exclude = excludesProcess(s,exclude);
                await head.speakText( s, {
                  lipsyncLang: cfg('voice-lipsync-lang'),
                  ttsVoice: cfg('voice-google-id'),
                  ttsLang: cfg('voice-google-id').substring(0,5),
                  ttsRate: cfg('voice-google-rate'),
                  ttsPitch: cfg('voice-google-pitch')
                }, addText.bind(null,node), exclude.excludes );
              }
            }
          }
        }

      }

    } else {
      console.error( await res.text() );
      throw new Error( json.error ? json.error.message : ''+res.status );
    }

  } catch (error) {
    if ( signal.aborted ) error = "aborted";
    console.error(error);
    addText( node, ' [' + error + ']');
  } finally {
    aiController = null; // Reset the AbortController instance
    if ( elevenSocket ) {
      elevenSpeak("",null);
    }
    microsoftSpeak(null);

    // When this marker has been reached, stop blinking
    head.speakMarker( () => {
      d3.selectAll('.blink').classed("blink",false);
    });
  }
}

// Llama

// Send messages to local LLama API and handle streamed response
async function llamaSendMessage(node,msgs) {

  // Create a new AbortController instance
  aiController = new AbortController();
  const signal = aiController.signal;

  // Chat completion
  try {
    // Message body
    const body = {
      model: cfg('ai-model'),
      messages: msgs.map( x => {
        const {role,content,name} = x;
        return (name ? {role,content,name} : {role,content});
      }),
      temperature: cfg('ai-openai-temperature'),
      presence_penalty: cfg('ai-openai-presence'),
      frequency_penalty: cfg('ai-openai-frequency'),
      max_tokens: cfg('ai-openai-output'),
      stream: true
    };
    const stop = cfg('ai-llama-stop');
    if ( stop && stop.length ) {
      body.stop = [ stop ];
    }
    const user = cfg('ai-llama-user');
    if ( user && user.length ) {
      body.user = user;
    }

    // Elements
    node.dataset.output = '';
    let tts = '';

    // Fetch the response from the Llama.cpp server
    tts = '';
    const res = await fetch( llamaChatCompletionsProxy, {
      method: "POST",
      mode: "cors",
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + await jwtGet()
      },
      body: JSON.stringify(body),
      signal
    });

    if ( res.ok ) {

      // Read the response as a stream of data
      const reader = res.body.getReader();
      const decoder = new TextDecoder("utf-8");
      let exclude = null; // Exclude object

      while (true) {
        let { done, value } = await reader.read();

        // Extract text data
        if ( value ) {
          decoder.decode(value).split('\n').forEach( data => {
            if ( data.startsWith("data: ") ) {
              let line = data.substring(6).trim();
              if ( line === '[DONE]' ) {
                done = true;
              } else {
                let obj;
                try {
                  obj = JSON.parse( line );
                } catch(error) {
                  throw new Error( "Error JSON parsing line: " + line );
                }

                if ( obj && obj.error ) throw new Error( obj.error.message || ''+res.status );
                node.dataset.output += obj?.choices?.[0]?.delta?.content || '';
                tts += obj?.choices?.[0]?.delta?.content || '';

              }
            }
          });
        }

        // Break, if done
        if (done) {


          if ( tts ) {
            if ( cfg('voice-type') === 'eleven' ) {
              await elevenSpeak( tts.trimStart() + " ", node );
            } else if ( cfg('voice-type') === 'microsoft' ) {
              await microsoftSpeak( tts.trimStart() + " ", node );
            } else {
              exclude = excludesProcess(tts,exclude);
              await head.speakText( tts, {
                lipsyncLang: cfg('voice-lipsync-lang'),
                ttsVoice: cfg('voice-google-id'),
                ttsLang: cfg('voice-google-id').substring(0,5),
                ttsRate: cfg('voice-google-rate'),
                ttsPitch: cfg('voice-google-pitch')
              }, addText.bind(null,node), exclude.excludes );
            }
          }

          break;
        }

        // Speak
        if ( tts ) {
          let idx = 0;
          ['.','!','?','\n'].forEach( x => {
            let tmp = tts.lastIndexOf(x);
            if ( tmp > idx ) idx = tmp + 1;
          });
          if ( idx > 10 ) {
            let s = tts.substring(0, idx);
            if ( s && s.length ) {
              tts = tts.substring(idx).trimStart();
              if ( cfg('voice-type') === 'eleven' ) {
                await elevenSpeak( s + " ", node );
              } else if ( cfg('voice-type') === 'microsoft' ) {
                await microsoftSpeak( s + " ", node );
              } else {
                exclude = excludesProcess(s,exclude);
                await head.speakText( s, {
                  lipsyncLang: cfg('voice-lipsync-lang'),
                  ttsVoice: cfg('voice-google-id'),
                  ttsLang: cfg('voice-google-id').substring(0,5),
                  ttsRate: cfg('voice-google-rate'),
                  ttsPitch: cfg('voice-google-pitch')
                }, addText.bind(null,node), exclude.excludes );
              }
            }
          }
        }

      }

    } else {
      console.error( await res.text() );
      throw new Error( json.error ? json.error.message : ''+res.status );
    }

  } catch (error) {
    if ( signal.aborted ) error = "aborted";
    console.error(error);
    addText( node, ' [' + error + ']');
  } finally {
    aiController = null; // Reset the AbortController instance
    if ( elevenSocket ) {
      elevenSpeak("",null);
    }
    microsoftSpeak(null);

    // When this marker has been reached, stop blinking
    head.speakMarker( () => {
      d3.selectAll('.blink').classed("blink",false);
    });
  }
}

// Build outgoing message for Google Gemini Pro
function geminiBuildMessage() {
  const msgs = [];

  const systems = [
    { sel: "[data-ai-gemini-system]", role: "system" },
    { sel: "[data-ai-gemini-user1]", role: "user" },
    { sel: "[data-ai-gemini-ai1]", role: "model" },
    { sel: "[data-ai-gemini-user2]", role: "user" },
    { sel: "[data-ai-gemini-ai2]", role: "model" }
  ];
  const session = d3.select(".session.selected");
  const input = d3.select("#input");
  const messages = session.selectAll(".message:not(.grayed)");

  const limitDialog = cfg('ai-gemini-dialog');
  const limitInput = cfg('ai-gemini-input');
  let dialogs = 0;
  let words = 0;

  // System messages
  systems.forEach( x => {
    const n = d3.select(x.sel).node();
    if ( n.value && n.value.length ) {
      if ( n.dataset.words ) {
        words += parseInt( n.dataset.words );
      } else {
        let wc = nWords( n.value );
        n.dataset.words = wc;
        words += wc;
      }
      msgs.push( { n: n, role: x.role, content: n.value } );
    }
  });

  // messages in reverse order
  const revmsgs = [];
  revmsgs.push( { n: input.node(), role: "user", content: input.property("value") } );
  words += nWords( input.property("value") );
  messages.nodes().reverse().forEach( (n) => {
    if ( dialogs < limitDialog && words < limitInput ) {
      let role;
      let val;
      if ( n.dataset.input && n.dataset.input.length ) {
        role = "user";
        val = n.dataset.input;
      } else if ( n.dataset.output && n.dataset.output.length ) {
        role = "model";
        val = n.dataset.output;
      }
      if ( role && val ) {
        revmsgs.push( { n: n, role: role, content: val } );
        if ( n.dataset.words ) {
          words += parseInt( n.dataset.words );
        } else {
          let wc = nWords( val );
          n.dataset.words = wc;
          words += wc;
        }
        dialogs++;
      }
    }
  });

  // Build message
  msgs.push( ...revmsgs.reverse() );

  return msgs;
}

// Send messages to Gemini API and handle streamed response
async function geminiSendMessage(node,msgs) {

  // Create a new AbortController instance
  aiController = new AbortController();
  const signal = aiController.signal;

  // Elements
  node.dataset.output = '';

  // Chat completion
  try {

    // Message body
    const body = {
      contents: [],
      safetySettings: [
        { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" },
        { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" },
        { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" },
        { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" }
      ],
      generationConfig: {
        temperature: cfg('ai-gemini-temperature'),
        maxOutputTokens: cfg('ai-gemini-output'),
        topP: cfg('ai-gemini-topp'),
        topK: cfg('ai-gemini-topk'),
        candidateCount: 1
      }
    };

    // Add messages
    // Make sure multiturn messages alternate between user and model
    let nextRole = 'user';
    msgs.forEach( x => {
      if ( x.role === "system" ) {
        body["systemInstruction"] = { "parts": [ { "text": x.content } ] };
      } else {
        if ( x.role !== nextRole ) {
          body.contents.push( { role: nextRole, "parts": { "text": "..." } } )
        }
        body.contents.push( { role: x.role, "parts": { "text": x.content } } );
        nextRole = x.role === 'user' ? 'model' : 'user';
      }
    });

    // Stop
    const stop = cfg('ai-gemini-stop');
    if ( stop && stop.length ) {
      body.generationConfig.stopSequences = [ stop ];
    }

    // Function calling
    const isMotionSelected = d3.select("#motion").classed("selected");
    if ( isMotionSelected ) {
      body.tools = [{
        functionDeclarations: [{
          "name": "move_body",
          "description": "Set the action, still pose, gesture and/or mood of your avatar's body in virtual world",
          "parameters": {
            "type": "object",
            "properties": {
              "action": {
                "type": "STRING",
                format: "enum",
                nullable: true,
                "enum": Object.keys(site.animations)
              },
              "stillpose": {
                "type": "STRING",
                format: "enum",
                nullable: true,
                "enum": Object.keys(site.poses)
              },
              "gesture": {
                "type": "STRING",
                format: "enum",
                nullable: true,
                "enum": Object.keys(site.gestures)
              },
              "mood": {
                "type": "STRING",
                format: "enum",
                nullable: true,
                "enum": Object.keys(head.animMoods)
              }
            },
            "required": []
          }
        }]
      }];
    }

    // Function loop
    let fn;
    do {

      fn = null;

      // Endpoint/proxy and authentication
      let url;
      let headers = { "Content-Type": "application/json; charset=utf-8" };
      const apikey = d3.select("#apikey-gemini").property("value");
      if ( apikey ) {
        url = geminiEndpoint + cfg("ai-model") + ':streamGenerateContent?alt=sse';
        headers["x-goog-api-key"] = apikey;
      } else {
        url = geminiProxy + cfg("ai-model") + ':streamGenerateContent?alt=sse';
        headers["Authorization"] = "Bearer " + await jwtGet()
      }

      // Fetch the response from the Vertex AI API
      const res = await fetch( url, {
        method: "POST",
        headers: headers,
        body: JSON.stringify(body),
        signal
      });

      if ( res.ok ) {

        // Read the response as a stream of data
        const reader = res.body.getReader();
        const decoder = new TextDecoder("utf-8");
        let tts = "";

        while (true) {
          const { done, value } = await reader.read();

          // Extract text data
          if ( value ) {
            decoder.decode(value).split('\n').forEach( data => {
              if ( data.startsWith("data: ") ) {
                let finish;
                try {
                  const obj = JSON.parse( data.substring(6).trim() );
                  const canditate = obj?.candidates?.[0];
                  finish = canditate?.finishReason || 'STOP';
                  fn = canditate?.content?.parts?.[0]?.functionCall;
                  if ( !fn ) {
                    node.dataset.markdown += canditate?.content?.parts?.[0]?.text || '';
                    node.dataset.output += canditate?.content?.parts?.[0]?.text || '';
                    tts += canditate?.content?.parts?.[0]?.text || '';
                  }

                } catch(error) {
                  console.log( "Error JSON parsing data: " + data );
                }
                if ( finish !== 'STOP' ) {
                  throw new Error("Stopped for " + finish + ".");
                }
              }
            });
          }

          // Speak and break when done
          if ( done ) {
            if ( tts ) {
              if ( cfg('voice-type') === 'eleven' ) {
                await elevenSpeak( tts + " ", node );
              } else if ( cfg('voice-type') === 'microsoft' ) {
                await microsoftSpeak( tts + " ", node );
              } else {
                await head.speakText( tts, {
                  lipsyncLang: cfg('voice-lipsync-lang'),
                  ttsVoice: cfg('voice-google-id'),
                  ttsLang: cfg('voice-google-id').substring(0,5),
                  ttsRate: cfg('voice-google-rate'),
                  ttsPitch: cfg('voice-google-pitch')
                }, addText.bind(null,node) );
              }
            }
            break;
          } else {
            let ndx = 0;
            ['.','!','?','\n'].forEach( x => {
              let tmp = tts.lastIndexOf(x);
              if ( tmp > ndx ) ndx = tmp + 1;
            });
            if ( ndx > 10 ) {
              if ( cfg('voice-type') === 'eleven' ) {
                await elevenSpeak( tts.substring(0, ndx) + " ", node );
              } else if ( cfg('voice-type') === 'microsoft' ) {
                await microsoftSpeak( tts.substring(0, ndx) + " ", node );
              } else {
                await head.speakText( tts.substring(0, ndx), {
                  lipsyncLang: cfg('voice-lipsync-lang'),
                  ttsVoice: cfg('voice-google-id'),
                  ttsLang: cfg('voice-google-id').substring(0,5),
                  ttsRate: cfg('voice-google-rate'),
                  ttsPitch: cfg('voice-google-pitch')
                }, addText.bind(null,node) );
              }
              tts = tts.substring(ndx).trimStart();
            }
          }

        }

      } else {
        console.error( await res.text() );
        throw new Error( json.error ? json.error.message : ''+res.status );
      }

      // Call function
      if ( fn ) {

        // Add function call to body
        body.contents.push( {
          role: "model",
          parts: [
            {
              functionCall: {
                name: "move_body",
                args: {
                  "action": fn.args?.["action"],
                  "stillpose": fn.args?.["stillpose"],
                  "gesture": fn.args?.["gesture"],
                  "mood": fn.args?.["mood"]
                }
              }
            }
          ]
        });

        // Call function
        try {
          motion(fn.args?.["action"], fn.args?.["stillpose"], fn.args?.["gesture"], fn.args?.["mood"]);
        } catch(motionError) {
          console.error(motionError);
        }

        // Add response
        body.contents.push({
          role: "function",
          parts: [
            {
              functionResponse: {
                name: "move_body",
                response: {
                  "status": "ok"
                }
              }
            }
          ]
        });

        // Instruct not to make another call
        body.toolConfig = { functionCallingConfig: { mode: "NONE" } };

      }

    } while( fn ); // Repeat, if this iteration was a function call

  } catch (error) {
    if ( signal.aborted ) error = "aborted";
    console.error(error);
    addText( node, ' [' + error + ']');
  } finally {
    aiController = null; // Reset the AbortController instance
    if ( elevenSocket ) {
      elevenSpeak("",null);
    }
    microsoftSpeak(null);

    // When this marker has been reached, stop blinking
    head.speakMarker( () => {
      d3.selectAll('.blink').classed("blink",false);
    });
  }
}


// Add new message to UI
function addMessage(user = false) {
  const session = d3.select(".session.selected");
  const message = session.append("div");
  message.classed("message", true)
    .classed("user",user)
    .classed("blink",!user);

  // Add selection
  const toolbarLeft = message.append("div");
  toolbarLeft.classed("toolbar-left", true);
  toolbarLeft.append("div")
    .classed("command",true)
    .classed("select", true )
    .on('click', function(ev) {
      let e = d3.select(this);
      let mode = !e.classed("selected");
      e.classed("selected", mode );
      d3.select(this.parentNode.parentNode).classed("grayed", mode );
    })
    .html(svgSelect);

  if ( user ) {
    const toolbarRight = message.append("div");
    toolbarRight.classed("toolbar-right", true);
    toolbarRight.append("div")
      .classed("command", true)
      .on('click', function(ev) {
        let n = this.parentNode.parentNode;
        const text = d3.select(n).attr('data-input');
        while(n) {
          let e = d3.select(n);
          e.classed("grayed",true);
          e.select(".select").classed("selected", true );
          n = n.nextSibling;
        }
        let node = d3.select("#input").property("value",text).dispatch("change").node();
        node.scrollTop = node.scrollHeight;
        node.focus();
      })
      .html(svgRepost);

  } else {
    const toolbarRight = message.append("div");
    toolbarRight.classed("toolbar-right", true);
    toolbarRight.append("div")
      .classed("command", true)
      .on('click', function(ev) {
        const e = d3.select(this.parentNode.parentNode);
        let text = e.attr("data-output");
        if ( text && text.length ) {
          if ( cfg('voice-type') === 'eleven' ) {
            elevenSpeak( text + " " );
            elevenSpeak( "" );
          } else if ( cfg('voice-type') === 'microsoft' ) {
            microsoftSpeak( text );
            microsoftSpeak( null );
          } else {
            head.speakText( text, {
              lipsyncLang: cfg('voice-lipsync-lang'),
              ttsVoice: cfg('voice-google-id'),
              ttsLang: cfg('voice-google-id').substring(0,5),
              ttsRate: cfg('voice-google-rate'),
              ttsPitch: cfg('voice-google-pitch')
            } );
          }
        }
      })
      .html(svgSpeak);

    toolbarRight.append("div")
      .classed("command", true)
      .on('click', function(ev) {
        head.stopSpeaking();
        // Abort the fetch request by calling abort() on the AbortController instance
        if (aiController) {
          aiController.abort();
          aiController = null;
        }
      })
      .html(svgStop);
  }

  message.append('p');

  const nodeSessions = d3.select("#right-sessions").node();
  nodeSessions.scrollTop = nodeSessions.scrollHeight;

  return message.node();
}

// Add text to session
function addText(node,s) {
  const nodeSessions = d3.select("#right-sessions").node();
  const onbottom = Math.abs( nodeSessions.scrollHeight - nodeSessions.scrollTop - nodeSessions.clientHeight) < 20;
  let last = node.lastElementChild;
  let markdown = (last.dataset.markdown || '') + s;
  let ndx = markdown.lastIndexOf('\n\n');
  if ( ndx === -1 ) {
    // Add to the existing paragraph
    last.innerHTML = dompurify.sanitize(marked.parseInline( markdown, markedOptions ));
    last.dataset.markdown = markdown;
  } else {
    // First part into the existing paragraph
    let md = markdown.substring(0, ndx);
    last.innerHTML = dompurify.sanitize(marked.parseInline( md, markedOptions ));
    last.dataset.markdown = md;

    // Last part into a new paragraph
    md = markdown.substring(ndx + 2);
    last = node.appendChild( document.createElement('p') );
    last.innerHTML = dompurify.sanitize(marked.parseInline( md, markedOptions ));
    last.dataset.markdown = md;
  }
  if ( onbottom ) nodeSessions.scrollTop = nodeSessions.scrollHeight;
}

// External speak
async function speak(s) {
  let node = addMessage(false);
  if ( cfg('voice-type') === 'eleven' ) {
    await elevenSpeak( s, node );
    await elevenSpeak( "", node );
  } else if ( cfg('voice-type') === 'microsoft' ) {
    await microsoftSpeak( s, node );
    await microsoftSpeak(null);
  } else {
    await head.speakText( s, {
      lipsyncLang: cfg('voice-lipsync-lang'),
      ttsVoice: cfg('voice-google-id'),
      ttsLang: cfg('voice-google-id').substring(0,5),
      ttsRate: cfg('voice-google-rate'),
      ttsPitch: cfg('voice-google-pitch')
    }, addText.bind(null,node) );
  }

  // When this marker has been reached, stop blinking
  head.speakMarker( () => {
    d3.selectAll('.blink').classed("blink",false);
  });
}
window.talkingheadSpeak = speak;

// Select an directory entry
function entrySelect() {
  reconnectEffect();
  const e = d3.select(this);
  const id = parseInt( e.attr("data-session") );
  d3.selectAll("[data-session]").classed('selected',false);
  e.classed('selected',true);
  loadConfig(id);
  i18nTranslate();
  d3.selectAll("[data-range]").dispatch("change");
}

// Move directory entry
function entryMove() {
  const e = d3.select(this);
  const n = e.node().parentElement; // Entry
  const id = parseInt( n.id.split('-')[1] );
  const directory = d3.select("#directory").node();
  const direction = e.attr("data-entry-move");
  let session = CFG.session;

  if ( direction === 'up' ) {

    // Swap configuration
    var tmp = CFG.sessions[id];
    CFG.sessions[id] = CFG.sessions[id-1];
    CFG.sessions[id-1] = tmp;

    // Swap directory entries
    d3.select(n)
      .property("id","entry-"+(id-1))
      .select("[data-session]")
      .attr("data-session",(id-1));
    d3.select(n.previousElementSibling)
      .property("id","entry-"+id)
      .select("[data-session]")
      .attr("data-session",id);
    directory.insertBefore(n,n.previousElementSibling);
    if ( session === id ) {
      session = id - 1;
    } else if ( session === (id-1) ) {
      session = id;
    }

    // Swap sessions ids (no need to swap their places)
    const s1 = d3.select("#session-"+id);
    const s2 = d3.select("#session-"+(id-1));
    s1.property("id","session-"+(id-1) );
    s2.property("id","session-"+id );

  } else if ( direction === 'down' ) {

    // Swap configuration
    var tmp = CFG.sessions[id];
    CFG.sessions[id] = CFG.sessions[id+1];
    CFG.sessions[id+1] = tmp;

    // Swap directory entries
    d3.select(n)
      .property("id","entry-"+(id+1))
      .select("[data-session]")
      .attr("data-session",(id+1));
    d3.select(n.nextElementSibling)
      .property("id","entry-"+id)
      .select("[data-session]")
      .attr("data-session",id);
    directory.insertBefore(n.nextElementSibling,n);
    if ( session === id ) {
      session = id + 1;
    } else if ( session === (id+1) ) {
      session = id;
    }

    // Swap sessions ids (no need to swap their places)
    const s1 = d3.select("#session-"+id);
    const s2 = d3.select("#session-"+(id+1));
    s1.property("id","session-"+(id+1) );
    s2.property("id","session-"+id );

  }

  // Save new configuration and load the session
  saveConfig();
  loadConfig(session);
  i18nTranslate();
  d3.selectAll("[data-range]").dispatch("change");
}

// Turn on effect
function turnOnEffect() {
  d3.selectAll('.starttransparent').nodes().forEach( panel => {
    const steps = 5 + Math.floor( Math.random() * 10 );
    const ops = [];
    for( let i=steps; i > 0; i-- ) {
      ops.push( { opacity: Math.random() } );
    }
    ops.push( { opacity: 1 } );
    const timing = {
      duration: Math.floor( Math.random() * 2000 ) + 300,
      fill: "forwards"
    };
    panel.animate(ops,timing);
  });
}

// Reconnect effect
function reconnectEffect() {

  // Turn off
  d3.selectAll("#avatar,#view").style("opacity","0");
  d3.select("#left").classed("glow",false);

  // Turn on
  setTimeout( () => {
    d3.selectAll("#avatar,#view").style("opacity","1");
    d3.select("#left").classed("glow",true);
  }, 1000);

}

// Talking Head
let head;
let morphTimer = null;

function headLoaded(firsttime=false) {

  // Populate emoji tester
  if ( firsttime ) {
    const elEmojis = d3.select("#emojis");
    for( let [em,x] of Object.entries(head.animEmojis) ) {
      elEmojis.append("div")
        .classed("emoji", true)
        .on('click', function(ev) {
          const e = d3.select(this);
          const name = e.text();

          // Clear timeouts
          if ( morphTimer ) {
            clearTimeout(morphTimer);
            morphTimer = null;
          }

          // Clear morphs
          head.getMorphTargetNames().forEach( mt => {
            const el = d3.select('#' + CSS.escape(mt));
            if ( !el.empty() ) {
              let v = testerGetValue(mt,null);
              if ( parseFloat(el.property("value")) !== v ) {
                el.property("value",v);
                d3.select(el.node().nextElementSibling).html(testerGetLabel(mt,v));
                head.setFixedValue(mt,testerGetFixedValue(mt,v));
              }
            }
          });

          const selected = e.classed("selected");
          d3.selectAll(".emoji").classed("selected",false);
          if ( selected ) {
            d3.selectAll(".emoji").classed("selected",false);
            testerUpdateMorphData();
          } else {
            e.classed("selected",true);

            // Animate
            head.playGesture(name,3);

            morphTimer = setTimeout( () => {
              morphTimer = null;
              const o = head.animEmojis[name];
              if ( o ) {
                head.getMorphTargetNames().forEach( mt => {
                  let val = null;
                  if ( o.vs &&  o.vs[mt] ) {
                    val = o.vs[mt][o.vs[mt].length-1];
                    if ( Array.isArray(val) ) val = val[val.length-1];
                  }
                  const el = d3.select('#' + CSS.escape(mt));
                  if ( !el.empty() ) {
                    let v = testerGetValue(mt,val);
                    if ( parseFloat(el.property("value")) !== v ) {
                      el.property("value",v);
                      d3.select(el.node().nextElementSibling).html(testerGetLabel(mt,v));
                      head.setFixedValue(mt,testerGetFixedValue(mt,v));
                    }
                  }
                });
                testerUpdateMorphData();
              }
            }, 3500);
          }
        })
        .html(em);
    }
  }

  // Populate morph targets (a.k.a. blend shapes)
  const elMorphs = d3.select("#morphs");
  elMorphs.html("");
  head.getMorphTargetNames().forEach( mt => {
    let v = head.getFixedValue(mt);

    const morph = elMorphs.append("div")
      .classed("row", true)
      .classed("morph", true);

    morph.append("div")
      .classed("text", true)
      .classed("label", true)
      .html(mt);

    morph.append("input")
      .property("id", mt)
      .property("type", "range")
      .property("min", testerGetMin(mt))
      .property("max", testerGetMax(mt))
      .property("step", testerGetStep(mt))
      .property("value", testerGetValue(mt,v))
      .on('input change keyup', function(ev) {
        const e = d3.select(this);
        const mt = e.property("id");
        let v = parseFloat(e.property("value"));
        d3.select(e.node().nextElementSibling).html(testerGetLabel(mt,v));
        head.setFixedValue(mt,testerGetFixedValue(mt,v));
        testerUpdateMorphData();
      });

    morph.append("div")
      .classed("text", true)
      .html(testerGetLabel(mt,v));
  });

  // Populate skinned meshes
  const elSkinnedMeshes = d3.select("#skinnedmeshes");
  elSkinnedMeshes.html("");
  head.armature.traverse( x => {
    if ( x.isSkinnedMesh ) {
      elSkinnedMeshes.append("div")
        .classed("command", true)
        .classed("selected", x.visible )
        .on('click', function(ev) {
          const e = d3.select(this);
          const sel = e.classed('selected');
          e.classed('selected', !sel)
          x.visible = !sel;
        })
        .html(x.name);
    }
  });

  // Populate dynamic bones
  const elBonesHelpers = d3.select("#boneshelpers");
  const elBonesSelect = d3.select("#bonesselect");
  elBonesHelpers.html("");
  elBonesSelect.html("");
  const names = head.dynamicbones?.getBoneNames();
  if ( names && names.length ) {

    // Director
    d3.select("#bonesfeaturedynamic")
      .on("click", null)
      .on("click", function(ev) {
        const e = d3.select(this);
        const sel = !e.classed('selected');
        e.classed('selected', sel);
        if ( sel === true ) {
          head.dynamicbones.start();
        } else {
          head.dynamicbones.stop();
        }
      })
      .classed("selected", true)
      .classed("disabled", false);
    d3.select("#bonesfeatureexcludes")
      .on("click", null)
      .on("click", function(ev) {
        const e = d3.select(this);
        const sel = !e.classed('selected');
        e.classed('selected', sel);
        head.dynamicbones.setOptionValue("isExcludes",sel);
      })
      .classed("selected", true)
      .classed("disabled", false);
    d3.select("#bonesfeaturepivots")
      .on("click", null)
      .on("click", function(ev) {
        const e = d3.select(this);
        const sel = !e.classed('selected');
        e.classed('selected', sel);
        head.dynamicbones.setOptionValue("isPivots",sel);
      })
      .classed("selected", true)
      .classed("disabled", false);
    d3.select("#bonesfeaturelimits")
      .on("click", null)
      .on("click", function(ev) {
        const e = d3.select(this);
        const sel = !e.classed('selected');
        e.classed('selected', sel);
        head.dynamicbones.setOptionValue("isLimits",sel);
      })
      .classed("selected", true)
      .classed("disabled", false);

    // Bone testers
    d3.selectAll(".bonetester").on("click", function(ev) {
      const e = d3.select(this);
      const name = d3.select(".dynamicbone.selected").text();
      const data = head?.dynamicbones?.data?.find( x => x.name === name );
      if ( data ) {
        let dir = 0;
        switch( e.text() ) {
          case 'x': dir = 0; break;
          case 'y': dir = 1; break;
          case 'z': dir = 2; break;
          case 't': dir = 3;
        }
        data.v[dir] = 100;
      }
    });

    // Show all
    elBonesHelpers.append("div")
      .property("id","bonehelpershowall")
      .classed("command", true)
      .on('click', function(ev) {
        const e = d3.select(this);
        const sel = e.classed('selected');
        e.classed('selected', !sel)
        if ( sel ) {
          head.dynamicbones.showHelpers(false);
        } else {
          head.dynamicbones.showHelpers(true);
        }
      })
      .html("ALL");

    names.forEach( (name,i) => {

      elBonesHelpers.append("div")
        .classed("command", true)
        .classed("bonehelper", true)
        .classed("selected", head.dynamicbones.getValue(name,"helper") || false )
        .on('click', function(ev) {
          const e = d3.select(this);
          const sel = e.classed('selected');
          e.classed('selected', !sel)
          head.dynamicbones.setValue(name,"helper",!sel);
        })
        .html(name);

      elBonesSelect.append("div")
        .classed("command", true)
        .classed("dynamicbone", true)
        .classed("selected", i === 0)
        .on('click', function(ev) {
          const e = d3.select(this);
          d3.selectAll(".dynamicbone").classed('selected',false);
          e.classed('selected',true);

          const type = head.dynamicbones.getValue(name,"type");
          d3.selectAll(".bonetype").classed('selected',false);
          d3.select("#bonetype"+type).classed('selected',true);

          let k = head.dynamicbones.getValue(name,"stiffness");
          d3.select("#stiffnesslock").classed("selected", !Number.isNaN(k) );
          if ( !Number.isNaN(k) ) {
            k = Array(4).fill(k);
          }
          d3.select('#stiffnessx').property('value', k[0] ).node().nextElementSibling.textContent = k[0];
          d3.select('#stiffnessy').property('value', k[1] ).node().nextElementSibling.textContent = k[1];
          d3.select('#stiffnessz').property('value', k[2] ).node().nextElementSibling.textContent = k[2];
          d3.select('#stiffnesst').property('value', k[3] ).node().nextElementSibling.textContent = k[3];

          let c = head.dynamicbones.getValue(name,"damping");
          d3.select("#dampinglock").classed("selected", !Number.isNaN(c) );
          if ( !Number.isNaN(k) ) {
            c = Array(4).fill(c);
          }
          d3.select('#dampingx').property('value', c[0] ).node().nextElementSibling.textContent = c[0];
          d3.select('#dampingy').property('value', c[1] ).node().nextElementSibling.textContent = c[1];
          d3.select('#dampingz').property('value', c[2] ).node().nextElementSibling.textContent = c[2];
          d3.select('#dampingt').property('value', c[3] ).node().nextElementSibling.textContent = c[3];

          const ext = head.dynamicbones.getValue(name,"external") || 1.0;
          d3.select('#external').property('value', ext ).node().nextElementSibling.textContent = ext;

          let l = head.dynamicbones.getValue(name,"limits");
          if ( l ) {
            if ( !l[0] ) l[0] = [null,null];
            if ( !l[1] ) l[1] = [null,null];
            if ( !l[2] ) l[2] = [null,null];
            if ( !l[3] ) l[3] = [null,null];
          } else {
            l = [[null,null],[null,null],[null,null],[null,null]];
          }
          l = l.flat();
          [ '#limitsx1','#limitsx2','#limitsy1','#limitsy2','#limitsz1','#limitsz2','#limitst1','#limitst2' ].forEach( (x,i) => {
            d3.select(x+'null').classed("selected", l[i] !== null );
            d3.select(x)
              .property("value",l[i])
              .classed("disabled", l[i] === null )
              .node().nextElementSibling.textContent = l[i] === null ? "" : (l[i] + ' m');
          });

          const deltal = head.dynamicbones.getValue(name,"deltaLocal") || [0,0,0];
          d3.select('#deltalx').property('value', deltal[0] ).node().nextElementSibling.textContent = deltal[0];
          d3.select('#deltaly').property('value', deltal[1] ).node().nextElementSibling.textContent = deltal[1];
          d3.select('#deltalz').property('value', deltal[2] ).node().nextElementSibling.textContent = deltal[2];

          const deltaw = head.dynamicbones.getValue(name,"deltaWorld") || [0,0,0];
          d3.select('#deltawx').property('value', deltaw[0] ).node().nextElementSibling.textContent = deltaw[0];
          d3.select('#deltawy').property('value', deltaw[1] ).node().nextElementSibling.textContent = deltaw[1];
          d3.select('#deltawz').property('value', deltaw[2] ).node().nextElementSibling.textContent = deltaw[2];

          const pivot = head.dynamicbones.getValue(name,"pivot") || false;
          d3.select("#pivot").classed("selected",pivot);

        })
        .html(name);
    });

    d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );

    // Change type
    d3.selectAll(".bonetype")
      .classed("disabled", false)
      .on('click', null)
      .on('click', function(ev) {
        const e = d3.select(this);
        d3.selectAll(".bonetype").classed('selected',false);
        e.classed('selected',true);
        const name = d3.select(".dynamicbone.selected").text();
        const type = e.text();
        head.dynamicbones.setValue(name,"type",type);
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Change stiffness
    d3.selectAll(".stiffness")
      .classed("disabled", false)
      .on('input.update change.update keyup.update', null)
      .on('input.update change.update keyup.update', function(ev) {
        const e = d3.select(this);
        const name = d3.select(".dynamicbone.selected").text();
        const lock = d3.select("#stiffnesslock").classed("selected");
        if ( lock ) {
          d3.selectAll(".stiffness").property('value',e.property('value')).nodes().forEach( x => {
            x.nextElementSibling.textContent = e.property('value');
          });
        }
        head.dynamicbones.setValue(name,"stiffness",[
          parseFloat( d3.select('#stiffnessx').property('value') ),
          parseFloat( d3.select('#stiffnessy').property('value') ),
          parseFloat( d3.select('#stiffnessz').property('value') ),
          parseFloat( d3.select('#stiffnesst').property('value') )
        ]);
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Change damping
    d3.selectAll(".damping")
      .classed("disabled", false)
      .on('input.update change.update keyup.update', null)
      .on('input.update change.update keyup.update', function(ev) {
        const e = d3.select(this);
        const name = d3.select(".dynamicbone.selected").text();
        const lock = d3.select("#dampinglock").classed("selected");
        if ( lock ) {
          d3.selectAll(".damping").property('value',e.property('value')).nodes().forEach( x => {
            x.nextElementSibling.textContent = e.property('value');
          });
        }
        head.dynamicbones.setValue(name,"damping",[
          parseFloat( d3.select('#dampingx').property('value') ),
          parseFloat( d3.select('#dampingy').property('value') ),
          parseFloat( d3.select('#dampingz').property('value') ),
          parseFloat( d3.select('#dampingt').property('value') )
        ]);
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Locks
    d3.selectAll("#stiffnesslock,#dampinglock")
      .classed("disabled", false)
      .on("click",null)
      .on('click', function(ev) {
        const e = d3.select(this);
        const sel = e.classed('selected');
        e.classed('selected', !sel);
        d3.select(this.parentNode).select("input").dispatch("change");
      });

    // Change external
    d3.select("#external")
      .classed("disabled", false)
      .on('input.update change.update keyup.update', null)
      .on('input.update change.update keyup.update', function(ev) {
        const name = d3.select(".dynamicbone.selected").text();
        head.dynamicbones.setValue(name,"external",
          parseFloat( d3.select('#external').property('value') )
        );
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Change limits
    d3.selectAll(".limits")
      .classed("disabled", false)
      .on('input.update change.update keyup.update', null)
      .on('input.update change.update keyup.update', function(ev) {
        let val = [ '#limitsx1','#limitsx2','#limitsy1','#limitsy2','#limitsz1','#limitsz2','#limitst1','#limitst2' ].map( (x,i) => {
          return d3.select(x+'null').classed("selected" ) ? parseFloat( d3.select(x).property("value") ) : null;
        });
        val = [
          [val[0],val[1]],
          [val[2],val[3]],
          [val[4],val[5]],
          [val[6],val[7]]
        ];
        val = val.map( x => ( x[0] === null && x[1] === null ) ? null : x );
        if ( val.every( x => x === null ) ) val = null;
        const name = d3.select(".dynamicbone.selected").text();
        head.dynamicbones.setValue(name, "limits", val );
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Change limits
    d3.selectAll(".limitsnull")
      .classed("disabled", false)
      .on('click', null)
      .on('click', function(ev) {
        const e = d3.select(this);
        const sel = e.classed('selected');
        e.classed('selected', !sel)
        if ( !sel ) {
          e.node().nextElementSibling.classList.remove('disabled');
        } else {
          e.node().nextElementSibling.classList.add('disabled');
        }
        d3.select(this.parentNode).select("input").dispatch("change");
      });

    // Change delta local
    d3.selectAll(".deltal")
      .classed("disabled", false)
      .on('input.update change.update keyup.update', null)
      .on('input.update change.update keyup.update', function(ev) {
        const name = d3.select(".dynamicbone.selected").text();
        head.dynamicbones.setValue(name,"deltaLocal",[
          parseFloat( d3.select('#deltalx').property('value') ),
          parseFloat( d3.select('#deltaly').property('value') ),
          parseFloat( d3.select('#deltalz').property('value') )
        ]);
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Change delta world
    d3.selectAll(".deltaw")
      .classed("disabled", false)
      .on('input.update change.update keyup.update', null)
      .on('input.update change.update keyup.update', function(ev) {
        const name = d3.select(".dynamicbone.selected").text();
        head.dynamicbones.setValue(name,"deltaWorld",[
          parseFloat( d3.select('#deltawx').property('value') ),
          parseFloat( d3.select('#deltawy').property('value') ),
          parseFloat( d3.select('#deltawz').property('value') )
        ]);
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Change pivot
    d3.select("#pivot")
      .classed("disabled", false)
      .on('click', null)
      .on('click', function(ev) {
        const e = d3.select(this);
        const sel = e.classed('selected');
        e.classed('selected', !sel)
        const name = d3.select(".dynamicbone.selected").text();
        head.dynamicbones.setValue(name,"pivot",!sel);
        d3.select('#bonesdata').property("value", JSON.stringify( head.dynamicbones.getConfig() ) );
      });

    // Select first bone
    d3.select('.dynamicbone').dispatch("click");

  } else {

    d3.selectAll("#bonesfeaturedynamic,#bonesfeatureexcludes,#bonesfeaturepivots,#bonesfeaturelimits,.bonetype,#pivot,#stiffnesslock,#dampinglock")
      .classed("selected", false)
      .classed("disabled", true)
      .on("click", null);

    let row = elBonesHelpers.append("div")
      .classed("row", true)
      .classed("bone", true);

    row.append("div")
      .classed("text", true)
      .classed("label", true)
      .html("N/A");

    row = elBonesSelect.append("div")
      .classed("row", true)
      .classed("bone", true);

    row.append("div")
      .classed("text", true)
      .classed("label", true)
      .html("N/A");

    d3.selectAll(".stiffness,.damping,#external,.limits,.deltal,.deltaw")
      .classed("disabled", true)
      .on('input.update change.update keyup.update', null);

    d3.select('#bonesdata').property("value", "");
  }

  // Unlock Web Audio API
  if ( firsttime ) {
    if ( head.audioCtx.state === 'suspended' ) {
      if ( 'ontouchstart' in window ) {
        let unlockWebAudioAPI = function() {
          head.audioCtx.resume().then( () => {
            document.body.removeEventListener('touchstart', unlockWebAudioAPI);
            document.body.removeEventListener('touchend', unlockWebAudioAPI);
          });
        };
        document.body.addEventListener('touchstart', unlockWebAudioAPI, false);
        document.body.addEventListener('touchend', unlockWebAudioAPI, false);
      }
    }

    setTimeout( () => {
      turnOnEffect();
      reconnectEffect();
      loadConfig();
    }, 1000);
  }
}

// Update progress bar
let progressTimeout;
function progressUpdate(ev) {
  if ( progressTimeout ) {
    clearTimeout(progressTimeout);
    progressTimeout = null;
  } else {
    d3.select("#loading").style("display","block");
  }
  let hideMs = 1000;
  if ( ev.lengthComputable ) {
    let val = Math.min(100,Math.round(ev.loaded/ev.total * 100 ));
    d3.select("#loading-top").style("clip-path", "inset(0 " + (100-val) + "% 0 0)");
    d3.selectAll("#loading-value").text( val + "%" );
    if ( val<100 ) hideMs = 3000;
  } else {
    d3.select("#loading-top").style("clip-path", "inset(0 0 0 0)");
    d3.selectAll("#loading-value").text( "" + ev.loaded );
  }
  progressTimeout = setTimeout( () => {
    d3.select("#loading").style("display","none");
    progressTimeout = null;
  }, hideMs);
}

// Update progress bar using media loading progress
function progressMedia() {
  let bf = this.buffered;
  let dur = this.duration;
  let state = this.readyState;
  if ( bf && bf.length && !Number.isNaN(dur) && dur < Infinity && state > 0 && state < 4) {
    progressUpdate( {
      loaded: bf.end(bf.length-1) - bf.start(bf.length-1),
      total: dur,
      lengthComputable: (dur > 0)
    });
  }
}

function errorShow(error) {
  console.error(error);
}

function testerUpdateMorphData() {
  let s = '{';
  for( let mt of head.getMorphTargetNames() ) {
    const el = d3.select('#' + CSS.escape(mt));
    if ( !el.empty() ) {
      const v = testerGetFixedValue( mt, parseFloat(el.property("value")) );
      if ( v !== null ) {
        s += (s.length > 1 ? ',' : '') + ' ' + mt + ': [' + v + ']';
      }
    }
  }
  s += ' }';
  d3.select('#morphdata').property("value",s).dispatch('change');
}

function testerGetValue(mt,v) {
  if ( mt.startsWith('headRotate') || mt.startsWith('bodyRotate') || mt.startsWith('eyesRotate') ) {
    return ( v === null || v === undefined ) ? 0 : v;
  } else {
    return ( v === null || v === undefined ) ? -0.1 : v;
  }
}

function testerGetFixedValue(mt,v) {
  if ( mt.startsWith('headRotate') || mt.startsWith('bodyRotate') || mt.startsWith('eyesRotate') ) {
    return ( v === 0 ? null : v);
  } else {
    return ( v >= 0 ? v : null);
  }
}

function testerGetMin(mt) {
  if ( mt.startsWith('headRotate') || mt.startsWith('bodyRotate') ) {
    return -0.5;
  } else if ( mt.startsWith('eyesRotate') ) {
    return -1;
  } else {
    return -0.1;
  }
}

function testerGetMax(mt) {
  return (mt.startsWith('headRotate') || mt.startsWith('bodyRotate')) ? 0.5 : 1;
}

function testerGetStep(mt) {
  return ( mt.startsWith('headRotate') || mt.startsWith('bodyRotate') || mt.startsWith('eyesRotate') ) ? 0.05 : 0.1;
}

function testerGetLabel(mt,v) {
  if ( mt.startsWith('headRotate') || mt.startsWith('bodyRotate') || mt.startsWith('eyesRotate') ) {
    return (v ? v : '');
  } else {
    return ( v>=0 ? v : '');
  }
}


// Script
let scriptIsRunning = false;
let scriptBlocks = [];
let scriptIndex = 0;

function scriptStatus(msg=null) {
  const el = d3.select("#scriptstatus");
  if ( msg ) {
    el.text( msg );
  } else if ( scriptIsRunning ) {
    el.text( "ACTIVE" );
  } else {
    const size = scriptBlocks.length;
    if ( size === 0 ) {
      el.text( "" );
    } else if ( scriptIndex === 0 ) {
      el.text( "READY" );
    } else if ( scriptIndex >= size ) {
      el.text( "END" );
    } else {
      el.text( (scriptIndex + 1) + "/" + size );
    }
  }
}

function scriptInit() {
  const script = d3.select("#script").property("value");
  scriptBlocks = script
    .replaceAll('\n\n\n','\n\n').trim()
    .split('\n\n')
    .map( x => x.trim().split('\n')
    .map( y => y.trim() ).filter( y => y.length) )
    .filter( y => y.length );
  scriptIndex = 0;
  scriptStatus();
}

function scriptCommand(ev) {
  if ( ev.altKey ) {
    if ( scriptIsRunning ) {
      if ( ev.keyCode === 37 ) { // Arrow left
        scriptIsRunning = false;
      }
    } else {
      const size = scriptBlocks.length;
      const status = d3.select("#scriptstatus");
      if ( ev.keyCode === 38 ) { // Arrow up
        if ( scriptIndex > 0 ) scriptIndex--;
        scriptStatus();
      } else if ( ev.keyCode === 39 ) { // Arrow right
        if ( scriptIndex < scriptBlocks.length ) {
          scriptIsRunning = true;
          scriptRun(scriptIndex);
          scriptStatus();
        }
      } else if ( ev.keyCode === 40 ) { // Arrow down
        if ( scriptIndex < scriptBlocks.length ) scriptIndex++;
        scriptStatus();
      }
    }
  }
}

window.sleep = ms => new Promise(r => setTimeout(r, ms));
const AsyncFunction = async function () {}.constructor;
async function scriptRun() {
  let node = null;
  for( let i=0; i < scriptBlocks[scriptIndex].length; i++ ) {
    if ( !scriptIsRunning ) break;

    let x = scriptBlocks[scriptIndex][i];

    if ( x.charAt(0) === '#' ) {
      // Comment, ignore
    } else if ( x.charAt(x.length-1) === ';' ) {
      // Function
      try {
        const fn = new AsyncFunction( x );
        await fn();
      } catch( err ) {
        console.error(err);
      }
    } else {

      // First text
      if ( !node ) {
        node = addMessage(false);
      } else {
        addText( node, " " );
      }

      if ( cfg('voice-type') === 'eleven' ) {
        await new Promise(resolve => {
          elevenOnProcessed = resolve;
          elevenSpeak( x + " ", node );
          elevenSpeak( "" );
        });
        elevenOnProcessed = null;
      } else if ( cfg('voice-type') === 'microsoft' ) {
        await new Promise(resolve => {
          microsoftSpeak( x, node, resolve );
        });
      } else {
        await head.speakText( x, {
          lipsyncLang: cfg('voice-lipsync-lang'),
          ttsVoice: cfg('voice-google-id'),
          ttsLang: cfg('voice-google-id').substring(0,5),
          ttsRate: cfg('voice-google-rate'),
          ttsPitch: cfg('voice-google-pitch')
        }, addText.bind(null,node) );
      }

      // Wait for it
      await new Promise(resolve => {
        head.speakMarker(resolve);
      });

    }
  }

  // Close connection
  if ( node ) {
    if ( elevenSocket ) {
      elevenSpeak("",null);
    }
    microsoftSpeak(null);
    d3.selectAll('.blink').classed("blink",false);
  }

  scriptIndex++;
  scriptIsRunning = false;
  scriptStatus();
}





// Page ready
document.addEventListener('DOMContentLoaded', async function(e) {

  // Add supported UI languages
  const eLanguages = d3.select("#languages");
  for ( const k of Object.keys(i18n) ) {
    eLanguages.append("div")
      .classed("command", true)
      .classed("selected", k === 'fi' )
      .attr("data-item","theme-lang")
      .attr("data-type","option")
      .attr("data-theme-lang",k)
      .attr("data-i18n-text",k);
  }

  // Add avatar links
  const eAvatars = d3.select("#avatars");
  for ( const [i,k] of Object.entries(Object.keys(site.avatars)) ) {
    eAvatars.append("div")
      .classed("command", true)
      .classed("selected", parseInt(i) === 0 )
      .attr("data-item","avatar-name")
      .attr("data-type","option")
      .attr("data-avatar-name", k )
      .attr("data-i18n-site","avatars-"+k);
  }

  // Add Google voices
  const eGoogleVoices = d3.select("#googlevoices");
  for ( const [i,k] of Object.entries(Object.keys(site.googleVoices)) ) {
    let o = site.googleVoices[k];
    eGoogleVoices.append("div")
      .classed("command", true)
      .classed("selected", parseInt(i) === 0 )
      .attr("data-item","voice-google-id")
      .attr("data-type","option")
      .attr("data-voice-google-id",o.id)
      .attr("data-i18n-site","googleVoices-"+k);
  }

  // Add ElevenLabs voices
  const eElevenVoices = d3.select("#elevenvoices");
  for ( const [i,k] of Object.entries(Object.keys(site.elevenVoices)) ) {
    let o = site.elevenVoices[k];
    eElevenVoices.append("div")
      .classed("command", true)
      .classed("selected", parseInt(i) === 0 )
      .attr("data-item","voice-eleven-id")
      .attr("data-type","option")
      .attr("data-voice-eleven-id",o.id)
      .attr("data-i18n-site","elevenVoices-"+k);
  }

  // Add Microsoft voices
  const eMicrosoftVoices = d3.select("#microsoftvoices");
  for ( const [i,k] of Object.entries(Object.keys(site.microsoftVoices)) ) {
    let o = site.microsoftVoices[k];
    eMicrosoftVoices.append("div")
      .classed("command", true)
      .classed("selected", parseInt(i) === 0 )
      .attr("data-item","voice-microsoft-id")
      .attr("data-type","option")
      .attr("data-voice-microsoft-lang",o.lang)
      .attr("data-voice-microsoft-id",o.id)
      .attr("data-voice-microsoft-viseme", o.viseme )
      .attr("data-i18n-site","microsoftVoices-"+k);
  }

  // Add views
  const eViews = d3.select("#views");
  for ( const [k,o] of Object.entries(site.views) ) {
    eViews.append("div")
      .classed("command", true)
      .attr("data-item","view-image")
      .attr("data-type","option")
      .attr("data-image-type",o.type)
      .attr("data-view-image",o.url)
      .attr("data-i18n-site","views-"+k);
  }

  // Add poses
  const ePoses = d3.select("#poses");
  for ( const [k,o] of Object.entries(site.poses) ) {
    ePoses.append("div")
      .classed("command", true)
      .attr("data-pose",o.url)
      .attr("data-i18n-site","poses-"+k);
  }

  // Add gestures
  if ( site.gestures ) {
    const eGestures = d3.select("#gestures");
    for ( const [k,o] of Object.entries(site.gestures) ) {
      eGestures.append("div")
        .classed("command", true)
        .attr("data-gesture",o.name)
        .attr("data-i18n-site","gestures-"+k);
    }
  }

  // Add animations
  const eAnimations = d3.select("#animations");
  for ( const [k,o] of Object.entries(site.animations) ) {
    eAnimations.append("div")
      .classed("command", true)
      .attr("data-animation",o.url)
      .attr("data-i18n-site","animations-"+k);
  }

  // Add impulse responses
  const eImpulses = d3.select("#impulses");
  for ( const [k,o] of Object.entries(site.impulses) ) {
    eImpulses.append("div")
      .classed("command", true)
      .attr("data-item","voice-reverb")
      .attr("data-type","option")
      .attr("data-voice-reverb",o.url)
      .attr("data-i18n-site","impulses-"+k);
  }

  // Add music
  const eMusic = d3.select("#music");
  for ( const [k,o] of Object.entries(site.music) ) {
    eMusic.append("div")
      .classed("command", true)
      .attr("data-item","voice-background")
      .attr("data-type","option")
      .attr("data-voice-background",o.url)
      .attr("data-i18n-site","music-"+k);
  }

  // Translate
  i18nTranslate();

  // Create the talking head avatar
  const nodeAvatar = document.getElementById('avatar');
  head = new TalkingHead( nodeAvatar, {
    jwtGet: jwtGet,
    ttsEndpoint: googleTTSProxy,
    cameraZoomEnable: true,
    cameraPanEnable: true,
    cameraView: 'full',
    avatarMood: 'neutral',
    lipsyncModules: ["en", "fi", "lt", "fr", "de"],
    // Stats display that can be used when testing performance
    statsNode: document.body,
    statsStyle: "position: fixed; bottom: 0px; left: 0px; cursor: pointer; opacity: 0.9; z-index: 10000;"
  });
  window.head = head;

  // Follow double clicks
  document.body.addEventListener('dblclick', function(e) {
    e = e || window.event;
    if ( !head.touchAt(e.clientX,e.clientY) ) {
      head.lookAt(e.clientX,e.clientY,500)
    }
  });

  // Media progress
  d3.select("#video").node().addEventListener("progress", progressMedia);

  // Auto resize textarea
  d3.selectAll("textarea").on("input.resize change.resize keyup.resize paste.resize", function(e) {
    let pos = this.parentElement.parentElement.scrollTop;
    this.style.height = '0px';
    this.style.height = this.scrollHeight+'px';
    this.parentElement.parentElement.scrollTop = pos;
  });

  // Reset moderation flag, score and word count
  d3.selectAll("textarea").on("input.moderate change.moderate paste.moderate", function(e) {
    delete this.dataset.flag;
    delete this.dataset.score;
    delete this.dataset.words;
  });

  // Send message
  d3.select("#input").on("keydown", async function(ev) {
    if ( ev.keyCode === 13 && !ev.shiftKey ) {
      ev.preventDefault();
      const e = d3.select("#input");
      let text = e.property("value");
      if ( head && text.length ) {

        if ( cfg('ai-model').startsWith("gpt") ) {
          const m = openaiBuildMessage();
          let flag = await openaiModerateMessage(m);

          const nodeInput = addMessage(true);
          nodeInput.dataset.input = text;
          addText(nodeInput,text);
          e.property("value","");

          if ( flag ) {
            console.info("Moderation flagged.");
            d3.select(nodeInput).classed("grayed", true );
            d3.select(nodeInput .select).classed("selected", true );
          } else {
            const nodeOutput = addMessage(false);
            openaiSendMessage(nodeOutput,m);
          }
        } else if ( cfg('ai-model').startsWith("gemini") ) {
          const m = geminiBuildMessage();

          const nodeInput = addMessage(true);
          nodeInput.dataset.input = text;
          addText(nodeInput,text);
          e.property("value","");

          const nodeOutput = addMessage(false);
          geminiSendMessage(nodeOutput,m);
        } else if ( cfg('ai-model').startsWith("llama") ) {
          const m = openaiBuildMessage();

          const nodeInput = addMessage(true);
          nodeInput.dataset.input = text;
          addText(nodeInput,text);
          e.property("value","");

          const nodeOutput = addMessage(false);
          llamaSendMessage(nodeOutput,m);
        } else if ( cfg('ai-model').startsWith("grok") ) {
          const m = openaiBuildMessage();

          const nodeInput = addMessage(true);
          nodeInput.dataset.input = text;
          addText(nodeInput,text);
          e.property("value","");

          const nodeOutput = addMessage(false);
          grokSendMessage(nodeOutput,m);
        }

      }
    }
  });

  // Change UI language
  d3.selectAll("[data-theme-lang]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-theme-lang]").classed('selected',false);
    e.classed('selected',true);
    const lang = e.attr("data-theme-lang");
    cfg('theme-lang',lang);
    i18nTranslate();
    d3.selectAll("[data-range]").dispatch("change");
  });

  // Speak test
  d3.select("#playtest").on("click", function(ev) {
    const e = d3.select("[data-voice-test]");
    let text = e.property("value");
    if ( head && text.length ) {
      if ( cfg('voice-type') === 'eleven' ) {
        elevenSpeak( text + " " );
        elevenSpeak( "" );
      } else if ( cfg('voice-type') === 'microsoft' ) {
        microsoftSpeak( text );
        microsoftSpeak( null );
      } else {
        const exclude = excludesProcess(text);
        head.speakText( text, {
          lipsyncLang: cfg('voice-lipsync-lang'),
          ttsVoice: cfg('voice-google-id'),
          ttsLang: cfg('voice-google-id').substring(0,5),
          ttsRate: cfg('voice-google-rate'),
          ttsPitch: cfg('voice-google-pitch')
        }, null, exclude.excludes );
      }
    }
  });

  // MP3 test file
  d3.select("#loadmp3-file").on("change", function(ev) {
    let file = ev.target.files[0];
    whisperLoadMP3(file);
    ev.target.value = '';
  });

  d3.select("#playmp3").on("click", function(ev) {
    if ( whisperAudio ) {
      head.speakAudio( whisperAudio, { lipsyncLang: whisperLipsyncLang } );
    }
  });

  // Repeat/mirror
  d3.selectAll("#pose-repeat,#animation-repeat,#gesture-repeat,#gesture-mirror").on("click", function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    e.classed("selected", !sel );
  });

  d3.select("#pause").on("click", function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    e.classed("selected", !sel );
    if ( !sel ) {
      head.stop();
    } else {
      head.start();
    }
  });

  d3.select("#slowdown").on("click", function(ev) {
    const e = d3.select(this);
    let k = 2 * head.getSlowdownRate();
    let t = i18nWord('Slow-motion') + ' x' + k;
    if ( k >= 16 ) {
      k = 1;
      t = i18nWord('Slow-motion');
    }
    e.classed("selected", (k > 1) ).text(t);
    head.setSlowdownRate( k );
  });

  d3.select("#autorotate").on("click", function(ev) {
    const e = d3.select(this);
    let k = head.getAutoRotateSpeed() + 10;
    let t = i18nWord('Panning') + ' ' + k;
    if ( k >= 60 ) {
      k = 0;
      t = i18nWord('Panning');
    }
    e.classed("selected", (k > 0) ).text(t);
    head.setAutoRotateSpeed( k );
  });

  // Avatar mood
  d3.selectAll("[data-mood]").on('click.command', function(ev) {
    const e = d3.select(this);
    head.setMood( e.attr('data-mood') );
  });

  // Avatar pose
  d3.selectAll("[data-pose]").on('click.command', async function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    const repeat = d3.select("#pose-repeat").classed("selected");
    d3.selectAll("[data-pose]").classed("selected",false);
    if ( sel ) {
      head.stopPose();
    } else {
      let pose = e.attr('data-pose');
      if ( pose !== 'FILE' ) {
        d3.selectAll("[data-pose='" + pose + "']").classed("selected",repeat);
        head.playPose( pose, progressUpdate, (repeat ? 600 : 5) );
      }
    }
  });

  // Avatar gesture
  d3.selectAll("[data-gesture]").on('click.command', async function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    const repeat = d3.select("#gesture-repeat").classed("selected");
    const mirror = d3.select("#gesture-mirror").classed("selected");
    d3.selectAll("[data-gesture]").classed("selected",false);
    if ( sel ) {
      head.stopGesture();
    } else {
      let g = e.attr('data-gesture');
      d3.selectAll("[data-gesture='" + g + "']").classed("selected",repeat);
      head.playGesture( g, (repeat ? 600 : 5), mirror );
    }
  });

  // Open pose file
  d3.select("#pose-file").on('change', function(ev) {
    let file = ev.target.files[0];
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload =  (readerEvent) => {
      let content = readerEvent.target.result;
      const repeat = d3.select("#pose-repeat").classed("selected");
      d3.selectAll("[data-pose='FILE']").classed("selected",repeat);
      head.playPose( content, progressUpdate, (repeat ? 600 : 5) );
    }
    ev.target.value = '';
  });

  d3.selectAll("[data-animation]").on('click.command', async function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    const repeat = d3.select("#animation-repeat").classed("selected");
    d3.selectAll("[data-animation]").classed("selected",false);
    if ( sel ) {
      head.stopAnimation();
    } else {
      let animation = e.attr('data-animation');
      if ( animation !== 'FILE' ) {
        d3.selectAll("[data-animation='" + animation + "']").classed("selected",repeat);
        head.playAnimation( animation, progressUpdate, (repeat ? 300 : 7) );
      }
    }
  });

  // Open animation file
  d3.select("#animation-file").on('change', function(ev) {
    let file = ev.target.files[0];
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = (ev2) => {
      let content = ev2.target.result;
      const repeat = d3.select("#animation-repeat").classed("selected");
      d3.selectAll("[data-animation='FILE']").classed("selected",repeat);
      head.playAnimation( content, progressUpdate, (repeat ? 300 : 7) );
    }
    ev.target.value = '';
  });

  // Save configuration changes
  d3.selectAll("[data-item]").on('click.config input.config change.config keyup.config', function(ev) {
    const e = d3.select(this);
    const item = e.attr("data-item");
    const type = e.attr("data-type");
    const range = e.attr("data-range");
    let value;
    if ( type === 'boolean' ) {
      value = !e.classed( "selected" );
      e.classed( "selected", value );
    } else if ( type === "option" ) {
      value = e.attr("data-"+item);
    } else if ( type === "value" ) {
      if ( range !== null ) {
        value = parseFloat( e.property("value") );
      } else {
        value = e.property("value");
      }
    }
    if ( !loadingConfig && value !== undefined && cfg( item ) !== value ) {
      cfg( item, value );
      saveConfig();
    }
  });

  // Change session
  d3.selectAll("[data-session]").on('click.command', entrySelect );
  d3.selectAll("[data-entry-move]").on('click.command', entryMove );

  d3.select("#selectall").on("click", function(ev) {
    const e = d3.select(this);
    const mode = !e.classed("selected");
    e.classed("selected", mode);

    d3.select(".session.selected").selectAll(".select").nodes().forEach( (n) => {
      const x = d3.select(n);
      if ( x.classed("selected") !== mode ) {
        x.dispatch("click");
      }
    });
  });

  // Reset range
  d3.selectAll("[data-range-reset]").on('click.command', function(ev) {
    const e = d3.select(this);
    const target = e.attr("data-target");
    const value = parseFloat(e.attr("data-range-reset"));
    d3.select("[data-"+target+"]").property("value",value).dispatch("change");
  });

  // Show value labels
  d3.selectAll("[data-range]").on('input.label change.label keyup.label', function(ev) {
    const e = d3.select(this);
    const n = e.node();
    const type = e.attr("data-range");
    let v = '';
    if ( type === 'float' ) {
      v = Math.round(100 * n.value) / 100;
    } else if ( type === 'second' ) {
      v = Math.round(100 * n.value) / 100 + ' s';
    } else if ( type === 'percentage' ) {
      v = Math.round(100 * n.value) + '%';
    } else if ( type === 'px' ) {
      v = n.value + ' px';
    } else if ( type === 'meter' ) {
      v = n.value + ' m';
    } else if ( type === 'radian' ) {
      v = n.value + ' rad';
    } else if ( type === 'word' ) {
      v = n.value + ' ' + i18nWord('words');
    } else if ( type === 'dialog' ) {
      v = n.value + ' ' + i18nWord('dialogs');
    }
    n.nextElementSibling.textContent = v;
  });

  d3.selectAll("[data-item='name']").on('input.command change.command keyup.command', function(ev) {
    const e = d3.select(this);
    const name = e.property("value");
    d3.select("#entry-"+CFG.session).select("div").text( name );
    d3.select("#name").text( name );
  });

  // Show/hide pages
  d3.selectAll("[data-show]").on('click.command', function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    const show = e.attr("data-show");
    const parts = show.split("-");

    d3.selectAll("[data-show*='" + parts[0] + "-']").classed("selected", false);
    d3.selectAll("[id*='" + parts[0] + "-']").classed("hidden", true);
    if ( !sel ) {
      e.classed("selected",true);
      d3.select('#'+show)
        .classed("hidden", false)
        .selectAll("textarea").dispatch("keyup");
      if ( parts[0] === 'right' ) {
        d3.select("#right-sessions").classed("hidden", true);
      }
    } else {
      if ( parts[0] === 'right' ) {
        d3.select("#right-sessions").classed("hidden", false);
      }
    }
  });

  d3.selectAll("[data-theme-ratio]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-theme-ratio]").classed('selected',false);
    e.classed('selected',true);
    const main = d3.select("#main");
    d3.selectAll("[data-theme-ratio]").nodes().forEach( x => {
      main.classed("ratio-" + x.dataset.themeRatio, false);
    });
    let ratio = 'ratio-' + d3.select("[data-theme-ratio].selected").attr('data-theme-ratio');
    main.classed(ratio, true);
  });

  d3.selectAll("[data-theme-layout]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-theme-layout]").classed('selected',false);
    e.classed('selected',true);
    const main = d3.select("#main");
    d3.selectAll("[data-theme-layout]").nodes().forEach( x => {
      main.classed("layout-" + x.dataset.themeLayout, false);
    });
    let layout = 'layout-' + d3.select("[data-theme-layout].selected").attr('data-theme-layout');
    main.classed(layout, true);
  });

  d3.selectAll("[data-theme-brightness]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-theme-brightness]").classed('selected',false);
    e.classed('selected',true);
    const body = d3.select("body");
    d3.selectAll("[data-theme-brightness]").nodes().forEach( x => {
      body.classed("theme-" + x.dataset.themeBrightness, false);
    });
    let theme = 'theme-' + d3.select("[data-theme-brightness].selected").attr('data-theme-brightness');
    body.classed(theme, true);
  });

  d3.selectAll("[data-avatar-name]").on('click.command', async function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-avatar-name]").classed('selected',false);
    let avatar = e.attr('data-avatar-name');
    d3.selectAll("[data-avatar-name='" + avatar + "']").classed("selected",true);
    if ( avatar === 'FILE' ) {
      // Do nothing
    } else if ( avatar === 'URL' ) {
      let url = cfg('avatar-url');
      d3.selectAll("[data-pose],[data-animation],#pause").classed("selected",false);
      const o = {
        url: url,
        body: cfg('avatar-body')
      };
      if ( url !== head.avatar?.url ) {
        try {
          await head.showAvatar( o, progressUpdate );
          headLoaded();
          d3.select("[data-camera-frame].selected").dispatch('click');
        } catch(error) {
          errorShow(error);
        }
      }
    } else {
      const name = e.attr('data-avatar-name');
      d3.selectAll("[data-pose],[data-animation],#pause").classed("selected",false);
      if ( site.avatars[name].url !== head.avatar?.url ) {
        try {
          await head.showAvatar( site.avatars[name], progressUpdate );
          headLoaded();
          d3.select("[data-camera-frame].selected").dispatch('click');
        } catch(error) {
          errorShow(error);
        }
      }
    }
  });

  // Open avatar file
  d3.select("#avatar-name-file").on('change', async function(ev) {
    let file = ev.target.files[0];
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = async (ev2) => {
      d3.selectAll("[data-pose],[data-animation],#pause").classed("selected",false);
      const o = {
        url: ev2.target.result,
        body: cfg('avatar-body')
      };
      try {
        await head.showAvatar( o, progressUpdate );
        headLoaded();
        d3.select("[data-camera-frame].selected").dispatch('click');
      } catch(error) {
        errorShow(error);
      }
    }
    ev.target.value = '';
  });

  d3.selectAll("[data-avatar-brightness],[data-avatar-contrast],[data-avatar-saturate]").on('input.command change.command keyup.command', function(ev) {
    const e = d3.select(this);
    let filters = [];
    d3.selectAll("[data-avatar-brightness],[data-avatar-contrast],[data-avatar-saturate]").nodes().forEach( node => {
      filters.push( (node.dataset.avatarBrightness || node.dataset.avatarContrast || node.dataset.avatarSaturate) + '(' + node.value + ')' );
    });
    d3.select("#avatar").style("filter", filters.join(' ') );
  });

  d3.selectAll("[data-avatar-body]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-avatar-body]").classed('selected',false);
    e.classed('selected',true);
  });

  d3.selectAll("[data-view-image]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-view-image]").classed('selected',false);
    let view = e.attr('data-view-image');
    d3.selectAll("[data-view-image='" + view + "']").classed("selected",true);
    if ( view === 'NONE' ) {
      CFG.sessions[CFG.session].view.image = "NONE";
      d3.select("#main")
        .classed("presence-video",false)
        .classed("presence-vr", true);
      d3.select("#view-video").attr("src","");
      d3.select("#video").node().load();
      d3.select("#view").style("background-image","none" );
    } else if ( view === 'FILE' ) {
      // Do nothing
    } else if ( view === 'URL' ) {
      CFG.sessions[CFG.session].view.image = "URL";
      d3.select("#main")
        .classed("presence-video", true)
        .classed("presence-vr", false);
      d3.select("#view-video").attr("src","");
      d3.select("#video").node().load();
      const image = cfg('view-url');
      d3.select("#view").style("background-image","url(" + image + ")" );
    } else {
      CFG.sessions[CFG.session].view.image = view;
      d3.select("#main")
        .classed("presence-video", true)
        .classed("presence-vr", false);
      const image = e.attr('data-view-image');
      const type = e.attr('data-image-type');
      if ( type.startsWith('video/') ) {
        d3.select("#view-video")
          .attr("src", image )
          .attr("type", type );
        d3.select("#video").node().load();
        d3.select("#view").style("background-image","none" );
      } else if ( type.startsWith('image/') ) {
        d3.select("#view-video").attr("src","");
        d3.select("#video").node().load();
        d3.select("#view").style("background-image","url(" + image + ")" );
      }
    }
  });

  // Open view image file
  d3.select("#view-image-file").on('change', function(ev) {
    let file = ev.target.files[0];
    var reader = new FileReader();
    if ( file.type.startsWith('image/') ) {
      d3.select("#main")
        .classed("presence-video", true)
        .classed("presence-vr", false);
      reader.readAsDataURL(file);
      reader.onload = (ev2) => {
        d3.select("#view-video").attr("src","");
        d3.select("#video").node().load();
        d3.select("#view").style("background-image","url(" + ev2.target.result + ")" );
      }
    } else if ( file.type.startsWith('video/') ) {
      d3.select("#main")
        .classed("presence-video", true)
        .classed("presence-vr", false);
      reader.readAsDataURL(file);
      reader.onload = (ev2) => {
        let content = ev2.target.result;
        d3.select("#view-video").attr("src",content);
        d3.select("#video").node().load();
      }
    }
    ev.target.value = '';
  });

  d3.select("[data-pose-movement]").on('input.command change.command keyup.command', function(ev) {
    let factor = parseFloat( d3.select("[data-pose-movement]").property("value") );
    head.opt.modelMovementFactor = factor;
    head.setPoseFromTemplate( head.poseCurrentTemplate );
  });

  d3.selectAll("[data-camera-frame]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-camera-frame]").classed('selected',false);
    const camera = e.attr('data-camera-frame');
    d3.selectAll("[data-camera-frame='" + camera + "']").classed("selected",true);
    const opt = {
      cameraX: parseFloat( cfg("camera-x") || 0 ),
      cameraY: parseFloat( cfg("camera-y") || 0 ),
      cameraDistance: parseFloat( cfg("camera-d") || 0 ),
      cameraRotateX: parseFloat( cfg("camera-rotx") || 0 ),
      cameraRotateY: parseFloat( cfg("camera-roty") || 0 )
    };
    head.setView( camera, opt );
  });

  d3.selectAll("[data-camera-x],[data-camera-y],[data-camera-d],[data-camera-rotx],[data-camera-roty]").on('input.command change.command keyup.command', function(ev) {
    d3.select("[data-camera-frame].selected").dispatch('click');
  });

  d3.select("[data-camera-fps]").on('input.command change.command keyup.command', function(ev) {
    let fps = parseFloat( d3.select("[data-camera-fps]").property("value") );
    head.opt.modelFPS = fps;
    head.animFrameDur = 1000 / fps;
  });

  function setLightingData() {
    let o = {
      lightAmbientColor: d3.select("[data-light-ambient-color]").property("value"),
      lightAmbientIntensity: parseFloat( d3.select("[data-light-ambient-intensity]").property("value") ),
      lightDirectColor: d3.select("[data-light-direct-color]").property("value"),
      lightDirectIntensity: parseFloat( d3.select("[data-light-direct-intensity]").property("value") ),
      lightDirectPhi: parseFloat( d3.select("[data-light-direct-phi]").property("value") ),
      lightDirectTheta: parseFloat( d3.select("[data-light-direct-theta]").property("value") ),
      lightSpotColor: d3.select("[data-light-spot-color]").property("value"),
      lightSpotIntensity: parseFloat( d3.select("[data-light-spot-intensity]").property("value") ),
      lightSpotPhi: parseFloat( d3.select("[data-light-spot-phi]").property("value") ),
      lightSpotTheta: parseFloat( d3.select("[data-light-spot-theta]").property("value") ),
      lightSpotDispersion: parseFloat( d3.select("[data-light-spot-dispersion]").property("value") )
    };
    d3.select('#lightingdata').property("value", JSON.stringify( o ) );
  }

  d3.selectAll("[data-light-ambient-color],[data-light-ambient-intensity]").on('input.command change.command keyup.command', function(ev) {
    const e = d3.select(this);
    let o = {
      lightAmbientColor: d3.select("[data-light-ambient-color]").property("value"),
      lightAmbientIntensity: parseFloat( d3.select("[data-light-ambient-intensity]").property("value") )
    };
    head.setLighting(o);
    setLightingData();
  });

  d3.selectAll("[data-light-direct-color],[data-light-direct-intensity],[data-light-direct-phi],[data-light-direct-theta]").on('input.command change.command keyup.command', function(ev) {
    const e = d3.select(this);
    let o = {
      lightDirectColor: d3.select("[data-light-direct-color]").property("value"),
      lightDirectIntensity: parseFloat( d3.select("[data-light-direct-intensity]").property("value") ),
      lightDirectPhi: parseFloat( d3.select("[data-light-direct-phi]").property("value") ),
      lightDirectTheta: parseFloat( d3.select("[data-light-direct-theta]").property("value") )
    };
    head.setLighting(o);
    setLightingData();
  });

  d3.selectAll("[data-light-spot-color],[data-light-spot-intensity],[data-light-spot-phi],[data-light-spot-theta],[data-light-spot-dispersion]").on('input.command change.command keyup.command', function(ev) {
    const e = d3.select(this);
    let o = {
      lightSpotColor: d3.select("[data-light-spot-color]").property("value"),
      lightSpotIntensity: parseFloat( d3.select("[data-light-spot-intensity]").property("value") ),
      lightSpotPhi: parseFloat( d3.select("[data-light-spot-phi]").property("value") ),
      lightSpotTheta: parseFloat( d3.select("[data-light-spot-theta]").property("value") ),
      lightSpotDispersion: parseFloat( d3.select("[data-light-spot-dispersion]").property("value") ),
    };
    head.setLighting(o);
    setLightingData();
  });

  d3.selectAll("[data-view-brightness],[data-view-contrast],[data-view-saturate],[data-view-blur]").on('input.command change.command keyup.command', function(ev) {
    const e = d3.select(this);
    let filters = [];
    d3.selectAll("[data-view-brightness],[data-view-contrast],[data-view-saturate],[data-view-blur]").nodes().forEach( node => {
      filters.push( (node.dataset.viewBrightness || node.dataset.viewContrast || node.dataset.viewSaturate || node.dataset.viewBlur) + '(' + node.value + (node.dataset.viewBlur ? 'px' :'') + ')' );
    });
    d3.select("#view").style("filter", filters.join(' ') );
  });

  d3.selectAll("[data-voice-type]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-type]").classed('selected',false);
    e.classed('selected',true);
    d3.select("#voice-google").style("display", (e.attr("data-voice-type") === 'google') ? "flex" : "none" );
    d3.select("#voice-eleven").style("display", (e.attr("data-voice-type") === 'eleven') ? "flex" : "none" );
    d3.select("#voice-microsoft").style("display", (e.attr("data-voice-type") === 'microsoft') ? "flex" : "none" );
    d3.select("#lipsync").style("display", (e.attr("data-voice-type") !== 'microsoft') ? "flex" : "none" );
    d3.select("#excludes").style("display", (e.attr("data-voice-type") === 'google') ? "flex" : "none" );
  });

  d3.selectAll("[data-voice-google-id]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-google-id]").classed('selected',false);
    e.classed('selected',true);
  });

  d3.selectAll("[data-voice-eleven-id]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-eleven-id]").classed('selected',false);
    e.classed('selected',true);
  });

  d3.selectAll("[data-voice-microsoft-id]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-microsoft-id]").classed('selected',false);
    e.classed('selected',true);
  });

  d3.selectAll("[data-voice-lipsync-lang]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-lipsync-lang]").classed('selected',false);
    e.classed('selected',true);
    d3.select('#visemeword').dispatch('change');
  });

  d3.select("[data-voice-mixerbg]").on('input.command change.command keyup.command', function(ev) {
    let gain = parseFloat( d3.select("[data-voice-mixerbg]").property("value") );
    head.setMixerGain( null, gain );
  });

  d3.select("[data-voice-mixerspeech]").on('input.command change.command keyup.command', function(ev) {
    let gain = parseFloat( d3.select("[data-voice-mixerspeech]").property("value") );
    head.setMixerGain( gain, null );
  });

  d3.selectAll("[data-voice-reverb]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-reverb]").classed('selected',false);
    e.classed('selected',true);
    let reverb = e.attr('data-voice-reverb');
    if ( reverb === 'NONE' ) {
      head.setReverb(null);
    } else {
      head.setReverb(reverb);
    }
  });

  d3.selectAll("[data-voice-background]").on('click.command', async function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-voice-background]").classed("selected",false);
    let background = e.attr('data-voice-background');
    d3.selectAll("[data-voice-background='" + background + "']").classed("selected",true);
    if ( background === 'NONE' ) {
      head.stopBackgroundAudio();
    } else if ( background === 'FILE' ) {
      // Do nothing
    } else {
      head.playBackgroundAudio(background);
    }
  });

  // Whisper
  d3.selectAll("[data-whisper-type]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-whisper-type]").classed('selected',false);
    e.classed('selected',true);
  });

  // Dynamic bones
  d3.select("[data-dynamicbones-sensitivity]").on('input.command change.command keyup.command', function(ev) {
    let factor = parseFloat( d3.select("[data-dynamicbones-sensitivity]").property("value") );
    head.dynamicbones.setOptionValue("sensitivityFactor",factor);
  });

  d3.select("[data-dynamicbones-movement]").on('input.command change.command keyup.command', function(ev) {
    let factor = parseFloat( d3.select("[data-dynamicbones-movement]").property("value") );
    head.dynamicbones.setOptionValue("movementFactor",factor);
  });

  // Open voice background file
  d3.select("#voice-background-file").on('change', function(ev) {
    let file = ev.target.files[0];
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = (ev2) => {
      let content = ev2.target.result;
      head.playBackgroundAudio(content);
    }
    ev.target.value = '';
  });

  d3.selectAll("[data-ai-model]").on('click.command', function(ev) {
    const e = d3.select(this);
    d3.selectAll("[data-ai-model]").classed('selected',false);
    e.classed('selected',true);
    d3.select("#ai-openai").style("display", (e.attr("data-ai-model").startsWith('gpt') ) ? "flex" : "none" );
    d3.select("#ai-gemini").style("display", (e.attr("data-ai-model").startsWith('gemini') ) ? "flex" : "none" );
    d3.select("#ai-grok").style("display", (e.attr("data-ai-model").startsWith('grok') ) ? "flex" : "none" );
    d3.select("#ai-llama").style("display", (e.attr("data-ai-model").startsWith('llama') ) ? "flex" : "none" );
  });

  d3.select("#session-duplicate").on('click', function(ev) {
    let id = CFG.sessions.length;
    let clone = JSON.parse( JSON.stringify(cfg()) );
    clone.name = 'Nimetön';
    CFG.sessions.push(clone);
    saveConfig();
    loadConfig(id);
  });

  d3.select("#session-delete").on('click', function(ev) {
    if ( CFG.sessions.length > 1 ) {
      let id = CFG.session;
      CFG.sessions.splice(id,1);
      if ( id > 0 ) id--;
      saveConfig();
      loadConfig(id);
    }
  });

  d3.select("#delete").on("click", function(ev) {
    d3.select("#selectall").classed("selected",false);
    d3.select(".session.selected").selectAll(".select.selected").nodes().forEach( (n) => {
      d3.select(n.parentElement.parentElement).remove();
    });
  });

  d3.select("#flag").on("click", async function(ev) {
    let msgs = openaiBuildMessage();
    let flag = await openaiModerateMessage(msgs);
  });

  d3.select('#copy').on('click', function(ev) {
    const session = d3.select(".session.selected");
    const ms = [];

    [ 'ai-openai-system', 'ai-openai-user1', 'ai-openai-ai1',
      'ai-openai-user2', 'ai-openai-ai2' ].forEach( x => {
      let p = cfg(x);
      if ( p && p.length ) ms.push(p);
    });

    session.selectAll(".message").nodes().forEach( (d) => {
      let e = d3.select(d);
      let md = e.attr("data-input") || e.attr("data-output");
      ms.push( md );
    } );

    const textarea = document.createElement('textarea');
    textarea.value = ms.join("\n\n");
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
  });

  d3.select(window).on('resize.updatesvg', function(ev) {
    d3.selectAll("textarea").dispatch("keyup");
  });

  d3.selectAll("#json").on("blur", function(ev) {
    let e = d3.select(this);
    const json = e.property("value");
    try {
      const c = JSON.parse( json );
      CFG.sessions[CFG.session] = c;
      saveConfig();
      loadConfig();
      i18nTranslate();
    } catch(error) {
      console.error(error);
    }
  });

  // Motion (experimental)
  d3.selectAll("#motion").on("click", function(ev) {
    const e = d3.select(this);
    const sel = e.classed("selected");
    e.classed("selected", !sel );
  });

  // Record
  d3.select('#record').on('click', async function(ev) {
    const x = d3.select(this);
    const mode = !x.classed("selected");
    x.classed("selected",mode);
    // recordingPlaySound();
    if ( mode ) {
      recordingRecord();
    } else {
      if ( recordingMediaRecorder ) {
        if ( recordingMediaRecorder.state === 'recording' ) {
          recordingIgnoreData = true;
          recordingMediaRecorder.stop();
        }
        recordingMediaRecorder.stream.getTracks().forEach( track => track.stop() );
        recordingMediaRecorder = null;
      }
      head.stopListening();
    }
  });

  // Viseme tester
  d3.select('#visemeword').on('input change keyup paste', function(ev) {
    const e = d3.select(this);
    const word = e.property("value");
    if ( word ) {
      try {
        const preprocessed = head.lipsyncPreProcessText( word, cfg('voice-lipsync-lang')  );
        const visemes = head.lipsyncWordsToVisemes( preprocessed, cfg('voice-lipsync-lang') );
        d3.select('#visemedata').property("value", JSON.stringify( visemes ) ).dispatch("change");
      } catch(error) {
        d3.select('#visemedata').property("value", error ).dispatch("change");
      }
    } else {
      d3.select('#visemedata').property("value", "" ).dispatch("change");
    }
  });

  // Script
  d3.select("#script").on('input change keyup paste', scriptInit);
  d3.select("body").on('keydown', scriptCommand);

  // Pause animation when not visible
  document.addEventListener("visibilitychange", async function (ev) {
    if (document.visibilityState === "visible") {
      if ( !d3.select("#pause").classed("selected") ) {
        head.start();
      }
    } else {
      head.stop();
    }
  });

  // API keys
  d3.select("#apikey-gtts").on('input change keyup paste', function (ev) {
    const e = d3.select(this);
    const apikey = e.property("value");

    // Change the API key
    if ( apikey ) {
      head.opt.ttsEndpoint = googleTTSEndpoint;
      head.opt.ttsApikey = apikey;
      head.opt.jwtGet = null;
    } else {
      head.opt.ttsEndpoint = googleTTSProxy;
      head.opt.ttsApikey = null;
      head.opt.jwtGet = jwtGet;
    }
  });

  d3.select("#apikey-microsoft").on('input change keyup paste', function (ev) {
    microsoftSpeak(null); // Closes the current connection, if any
  });

  try {

    // Get token
    jwt = await jwtGet();

    // Show last avatar
    let o = {};
    let name = cfg('avatar-name') || Object.values(site.avatars)[0].name;
    if ( site.avatars.hasOwnProperty(name) ) {
      o = site.avatars[name];
    } else if ( name === 'URL' ) {
      o.url = cfg('avatar-url');
      o.body = cfg('avatar-body');
    } else {
      o =  Object.values(site.avatars)[0];
    }
    await head.showAvatar( o, progressUpdate);
    headLoaded(true);
  } catch (error) {
    errorShow(error);

    // Backup plan: show the first avatar in site config
    await head.showAvatar( Object.values(site.avatars)[0], progressUpdate);
    headLoaded(true);
  }

});

</script>
</head>
<body class="theme-dark">

  <div id="main" class="ratio-wide layout-land presence-video">

    <div id="left" class="noselect nodrag">

      <div id="view">
        <video id="video" autoplay loop muted playsinline>
          <source id="view-video" src="" type="video/mp4">
        </video>
      </div>

      <div id="avatar"></div>

      <div class="border starttransparent"></div>

      <div id="loading">
        <div id="loading-back"></div>
        <div id="loading-top"></div>
        <div id="loading-value"></div>
      </div>

    </div>


    <div id="right">

      <div class="pages">

        <div class="row noselect nodrag">

          <div class="command starttransparent" data-i18n-title="Session" data-show="right-session">
            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <path d="M2 6C2 5.44772 2.44772 5 3 5H21C21.5523 5 22 5.44772 22 6C22 6.55228 21.5523 7 21 7H3C2.44772 7 2 6.55228 2 6Z" fill="currentColor" />
              <path d="M2 12.0322C2 11.4799 2.44772 11.0322 3 11.0322H21C21.5523 11.0322 22 11.4799 22 12.0322C22 12.5845 21.5523 13.0322 21 13.0322H3C2.44772 13.0322 2 12.5845 2 12.0322Z" fill="currentColor" />
              <path d="M3 17.0645C2.44772 17.0645 2 17.5122 2 18.0645C2 18.6167 2.44772 19.0645 3 19.0645H21C21.5523 19.0645 22 18.6167 22 18.0645C22 17.5122 21.5523 17.0645 21 17.0645H3Z" fill="currentColor" />
            </svg>
          </div>

          <div id="name" class="text starttransparent">
            Nimet&ouml;n
          </div>

          <div class="filler"></div>

          <div class="command starttransparent" data-i18n-title="Manuscript" data-show="right-ai">
            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <path d="M7 18H17V16H7V18Z" fill="currentColor" />
              <path d="M17 14H7V12H17V14Z" fill="currentColor" />
              <path d="M7 10H11V8H7V10Z" fill="currentColor" />
              <path fill-rule="evenodd" clip-rule="evenodd" d="M6 2C4.34315 2 3 3.34315 3 5V19C3 20.6569 4.34315 22 6 22H18C19.6569 22 21 20.6569 21 19V9C21 5.13401 17.866 2 14 2H6ZM6 4H13V9H19V19C19 19.5523 18.5523 20 18 20H6C5.44772 20 5 19.5523 5 19V5C5 4.44772 5.44772 4 6 4ZM15 4.10002C16.6113 4.4271 17.9413 5.52906 18.584 7H15V4.10002Z" fill="currentColor" />
            </svg>
          </div>
          <div class="command starttransparent" data-i18n-title="Location" data-show="right-view">
            <svg viewBox="-1 -1 26 26" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M7 7C5.34315 7 4 8.34315 4 10C4 11.6569 5.34315 13 7 13C8.65685 13 10 11.6569 10 10C10 8.34315 8.65685 7 7 7ZM6 10C6 9.44772 6.44772 9 7 9C7.55228 9 8 9.44772 8 10C8 10.5523 7.55228 11 7 11C6.44772 11 6 10.5523 6 10Z" fill="currentColor" />
              <path fill-rule="evenodd" clip-rule="evenodd" d="M3 3C1.34315 3 0 4.34315 0 6V18C0 19.6569 1.34315 21 3 21H21C22.6569 21 24 19.6569 24 18V6C24 4.34315 22.6569 3 21 3H3ZM21 5H3C2.44772 5 2 5.44772 2 6V18C2 18.5523 2.44772 19 3 19H7.31374L14.1924 12.1214C15.364 10.9498 17.2635 10.9498 18.435 12.1214L22 15.6863V6C22 5.44772 21.5523 5 21 5ZM21 19H10.1422L15.6066 13.5356C15.9971 13.145 16.6303 13.145 17.0208 13.5356L21.907 18.4217C21.7479 18.7633 21.4016 19 21 19Z" fill="currentColor" />
            </svg>
          </div>
          <div class="command starttransparent" data-i18n-title="Avatar" data-show="right-avatar">
            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M16 7C16 9.20914 14.2091 11 12 11C9.79086 11 8 9.20914 8 7C8 4.79086 9.79086 3 12 3C14.2091 3 16 4.79086 16 7ZM14 7C14 8.10457 13.1046 9 12 9C10.8954 9 10 8.10457 10 7C10 5.89543 10.8954 5 12 5C13.1046 5 14 5.89543 14 7Z" fill="currentColor" />
              <path d="M16 15C16 14.4477 15.5523 14 15 14H9C8.44772 14 8 14.4477 8 15V21H6V15C6 13.3431 7.34315 12 9 12H15C16.6569 12 18 13.3431 18 15V21H16V15Z" fill="currentColor" />
            </svg>
          </div>
          <div class="command starttransparent" data-i18n-title="Bones" data-show="right-bones">
            <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
              <path d="m19.1072 11.2565c.7557.0637 1.5333-.1936 2.1115-.7718 1.0417-1.0417 1.0417-2.73065 0-3.77236-.6245-.62448-1.5546-.8015-2.1996-.84313-.5717 0-.8883-.34038-.8883-.88834-.0416-.64494-.2187-1.57511-.8431-2.19959-1.0418-1.04171-2.7307-1.04171-3.7724 0-.5782.57821-.8355 1.35582-.7718 2.11147.0867 1.0289.1799 2.25278-.5502 2.9829l-4.31765 4.31765c-.73012.7301-1.954.6369-2.9829.5502-.75565-.0637-1.53326.1936-2.11147.7718-1.04171 1.0417-1.04171 2.7306 0 3.7724.62448.6244 1.55465.8015 2.19959.8431.61071 0 .88835.3692.88835.8883.04162.645.21864 1.5751.84312 2.1996 1.04171 1.0417 2.73066 1.0417 3.77236 0 .5782-.5782.8355-1.3558.7718-2.1115-.0867-1.0288-.1799-2.2527.5502-2.9829l4.3177-4.3176c.7301-.7301 1.954-.6369 2.9828-.5502z" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
            </svg>
          </div>
          <div class="command starttransparent" data-i18n-title="Light" data-show="right-light">
            <svg viewBox="1 0 26 25" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="currentColor" />
              <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" fill="currentColor" />
            </svg>
          </div>
          <div class="command starttransparent" data-i18n-title="Camera" data-show="right-camera">
            <svg viewBox="-1 -1 26 26" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M4 4.5V6.5H12V7.5H3C1.34315 7.5 0 8.84315 0 10.5V16.5C0 18.1569 1.34315 19.5 3 19.5H15C16.5731 19.5 17.8634 18.2892 17.9898 16.7487L24 17.5V9.5L17.9898 10.2513C17.8634 8.71078 16.5731 7.5 15 7.5H14V5.5C14 4.94772 13.5523 4.5 13 4.5H4ZM18 12.2656V14.7344L22 15.2344V11.7656L18 12.2656ZM16 10.5C16 9.94772 15.5523 9.5 15 9.5H3C2.44772 9.5 2 9.94772 2 10.5V16.5C2 17.0523 2.44772 17.5 3 17.5H15C15.5523 17.5 16 17.0523 16 16.5V10.5Z" fill="currentColor" />
            </svg>
          </div>
          <div class="command starttransparent" data-i18n-title="Audio" data-show="right-audio">
            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M12 8.95439L17.6352 7.82736C19.0102 7.55235 20 6.34501 20 4.94274C20 3.08637 18.3017 1.69406 16.4813 2.05813L12.4117 2.87206C11.0094 3.15252 10 4.38376 10 5.8138V13.8895C9.41165 13.5492 8.72857 13.3544 8 13.3544C5.79086 13.3544 4 15.1453 4 17.3544C4 19.5635 5.79086 21.3544 8 21.3544C10.2091 21.3544 12 19.5635 12 17.3544V8.95439ZM16.8736 4.01929L12.8039 4.83322C12.3365 4.92671 12 5.33712 12 5.8138V6.91479L17.2429 5.8662C17.6831 5.77816 18 5.39165 18 4.94274C18 4.34846 17.4563 3.90274 16.8736 4.01929ZM10 17.3544C10 16.2498 9.10457 15.3544 8 15.3544C6.89543 15.3544 6 16.2498 6 17.3544C6 18.459 6.89543 19.3544 8 19.3544C9.10457 19.3544 10 18.459 10 17.3544Z" fill="currentColor" />
            </svg>
          </div>

        </div>

        <div id="right-ai" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="AI"></div>
            <div class="column">
              <div class="rowWrap">
                <div class="command selected" data-item="ai-model" data-type="option" data-ai-model="gpt-4.1-mini">GPT-4.1-mini</div>
                <div class="command" data-item="ai-model" data-type="option" data-ai-model="gpt-4.1">GPT-4.1</div>
                <div class="command" data-item="ai-model" data-type="option" data-ai-model="grok-3">Grok-3</div>
                <div class="command" data-item="ai-model" data-type="option" data-ai-model="gemini-2.5-flash">Gemini-2.5-flash</div>
                <div class="command" data-item="ai-model" data-type="option" data-ai-model="llama">Llama</div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div id="ai-openai" class="column">
            <div class="row">
              <div class="text label" data-i18n-text="System"></div>
              <textarea data-i18n-placeholder="ai-system" rows="1" autocapitalize="sentences" data-item="ai-openai-system" data-type="value" data-ai-openai-system=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example1"></div>
              <textarea data-i18n-placeholder="ai-user1" rows="1" autocapitalize="sentences" data-item="ai-openai-user1" data-type="value" data-ai-openai-user1=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai1" rows="1" autocapitalize="sentences" data-item="ai-openai-ai1" data-type="value" data-ai-openai-ai1=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example2"></div>
              <textarea data-i18n-placeholder="ai-user2" rows="1" autocapitalize="sentences" data-item="ai-openai-user2" data-type="value" data-ai-openai-user2=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai2" rows="1" autocapitalize="sentences" data-item="ai-openai-ai2" data-type="value" data-ai-openai-ai2=""></textarea>
            </div>

            <div class="vbar"></div>

            <div class="row">
              <div class="text label" data-i18n-text="Adjust"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-openai-temperature" data-range-reset="1.0">
                    <svg viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M336 972.8c-60.8-128-28.8-201.6 19.2-268.8 51.2-76.8 64-150.4 64-150.4s41.6 51.2 25.6 134.4c70.4-80 83.2-208 73.6-256 160 112 230.4 358.4 137.6 537.6 492.8-281.6 121.6-700.8 57.6-745.6 22.4 48 25.6 128-19.2 166.4-73.6-281.6-256-336-256-336 22.4 144-76.8 300.8-172.8 419.2-3.2-57.6-6.4-96-38.4-153.6-6.4 105.6-86.4 188.8-108.8 294.4C89.6 758.4 140.8 860.8 336 972.8L336 972.8z"  fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="0" max="2" step="0.1" value="1.0" data-range="float" data-item="ai-openai-temperature" data-type="value" data-ai-openai-temperature="">
                  <div class="text">1.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-openai-presence" data-range-reset="0.0">
                    <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M9.00967 5.12761H11.0097C12.1142 5.12761 13.468 5.89682 14.0335 6.8457L16.5089 11H21.0097C21.562 11 22.0097 11.4477 22.0097 12C22.0097 12.5523 21.562 13 21.0097 13H16.4138L13.9383 17.1543C13.3729 18.1032 12.0191 18.8724 10.9145 18.8724H8.91454L12.4138 13H5.42485L3.99036 15.4529H1.99036L4.00967 12L4.00967 11.967L2.00967 8.54712H4.00967L5.44417 11H12.5089L9.00967 5.12761Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="-1" max="2" step="0.1" value="0.0" data-range="float" data-item="ai-openai-presence" data-type="value" data-ai-openai-presence="">
                  <div class="text">0.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-openai-frequency" data-range-reset="0.0">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M21.2635 2.29289C20.873 1.90237 20.2398 1.90237 19.8493 2.29289L18.9769 3.16525C17.8618 2.63254 16.4857 2.82801 15.5621 3.75165L4.95549 14.3582L10.6123 20.0151L21.2189 9.4085C22.1426 8.48486 22.338 7.1088 21.8053 5.99367L22.6777 5.12132C23.0682 4.7308 23.0682 4.09763 22.6777 3.70711L21.2635 2.29289ZM16.9955 10.8035L10.6123 17.1867L7.78392 14.3582L14.1671 7.9751L16.9955 10.8035ZM18.8138 8.98525L19.8047 7.99429C20.1953 7.60376 20.1953 6.9706 19.8047 6.58007L18.3905 5.16586C18 4.77534 17.3668 4.77534 16.9763 5.16586L15.9853 6.15683L18.8138 8.98525Z" fill="currentColor" />
                      <path d="M2 22.9502L4.12171 15.1717L9.77817 20.8289L2 22.9502Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="-2" max="2" step="0.1" value="0.0" data-range="float" data-item="ai-openai-frequency" data-type="value" data-ai-openai-frequency="">
                  <div class="text">0.0</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Limits"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-openai-dialog" data-range-reset="4">
                    <svg viewBox="-100 -100 1224 1224" xmlns="http://www.w3.org/2000/svg">
                      <path d="M170.65984 42.65984l682.65984 0q53.00224 0 90.50112 37.49888t37.49888 90.50112l0 469.34016q0 53.00224-37.49888 90.50112t-90.50112 37.49888l-341.34016 0-298.65984 213.34016 0-213.34016-42.65984 0q-53.00224 0-90.50112-37.49888t-37.49888-90.50112l0-469.34016q0-53.00224 37.49888-90.50112t90.50112-37.49888zM853.34016 128l-682.65984 0q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 469.34016q0 17.67424 12.4928 30.16704t30.16704 12.4928l128 0 0 132.99712 185.99936-132.99712 368.66048 0q17.67424 0 30.16704-12.4928t12.4928-30.16704l0-469.34016q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="2" max="10" step="2" value="4" data-range="dialog" data-item="ai-openai-dialog" data-type="value" data-ai-openai-dialog="">
                  <div class="text">4 sanomaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-openai-input" data-range-reset="2000">
                    <svg viewBox="-1 -1 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C9 2.34315 10.3431 1 12 1C13.6569 1 15 2.34315 15 4V12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12V4ZM13 4V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4Z" fill="currentColor" />
                      <path d="M18 12C18 14.973 15.8377 17.441 13 17.917V21H17V23H7V21H11V17.917C8.16229 17.441 6 14.973 6 12V9H8V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V9H18V12Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="2000" data-range="word" data-item="ai-openai-input" data-type="value" data-ai-openai-input="">
                  <div class="text">1000 sanaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-openai-output" data-range-reset="1000">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.939C14.2091 18.939 16 17.1482 16 14.939C16 12.7299 14.2091 10.939 12 10.939C9.79086 10.939 8 12.7299 8 14.939C8 17.1482 9.79086 18.939 12 18.939ZM12 16.939C13.1046 16.939 14 16.0436 14 14.939C14 13.8345 13.1046 12.939 12 12.939C10.8954 12.939 10 13.8345 10 14.939C10 16.0436 10.8954 16.939 12 16.939Z" fill="currentColor" />
                      <path d="M12 9.04401C13.1046 9.04401 14 8.14858 14 7.04401C14 5.93944 13.1046 5.04401 12 5.04401C10.8954 5.04401 10 5.93944 10 7.04401C10 8.14858 10.8954 9.04401 12 9.04401Z" fill="currentColor" />
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C5.34315 1 4 2.34315 4 4V20C4 21.6569 5.34315 23 7 23H17C18.6569 23 20 21.6569 20 20V4C20 2.34315 18.6569 1 17 1H7ZM17 3H7C6.44772 3 6 3.44772 6 4V20C6 20.5523 6.44772 21 7 21H17C17.5523 21 18 20.5523 18 20V4C18 3.44772 17.5523 3 17 3Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="1000" data-range="word" data-item="ai-openai-output" data-type="value" data-ai-openai-output="">
                  <div class="text">2000 sanaa</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-stop"></div>
              <input type="text" data-i18n-placeholder="ai-stopword" data-item="ai-openai-stop" data-type="value" data-ai-openai-stop="">
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-user"></div>
              <input type="text" data-i18n-placeholder="ai-username" data-item="ai-openai-user" data-type="value" data-ai-openai-user="">
            </div>
          </div>

          <div id="ai-grok" class="column">
            <div class="row">
              <div class="text label" data-i18n-text="System"></div>
              <textarea data-i18n-placeholder="ai-system" rows="1" autocapitalize="sentences" data-item="ai-openai-system" data-type="value" data-ai-openai-system=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example1"></div>
              <textarea data-i18n-placeholder="ai-user1" rows="1" autocapitalize="sentences" data-item="ai-openai-user1" data-type="value" data-ai-openai-user1=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai1" rows="1" autocapitalize="sentences" data-item="ai-openai-ai1" data-type="value" data-ai-openai-ai1=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example2"></div>
              <textarea data-i18n-placeholder="ai-user2" rows="1" autocapitalize="sentences" data-item="ai-openai-user2" data-type="value" data-ai-openai-user2=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai2" rows="1" autocapitalize="sentences" data-item="ai-openai-ai2" data-type="value" data-ai-openai-ai2=""></textarea>
            </div>

            <div class="vbar"></div>

            <div class="row">
              <div class="text label" data-i18n-text="Adjust"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-grok-temperature" data-range-reset="1.0">
                    <svg viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M336 972.8c-60.8-128-28.8-201.6 19.2-268.8 51.2-76.8 64-150.4 64-150.4s41.6 51.2 25.6 134.4c70.4-80 83.2-208 73.6-256 160 112 230.4 358.4 137.6 537.6 492.8-281.6 121.6-700.8 57.6-745.6 22.4 48 25.6 128-19.2 166.4-73.6-281.6-256-336-256-336 22.4 144-76.8 300.8-172.8 419.2-3.2-57.6-6.4-96-38.4-153.6-6.4 105.6-86.4 188.8-108.8 294.4C89.6 758.4 140.8 860.8 336 972.8L336 972.8z"  fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="0" max="2" step="0.1" value="1.0" data-range="float" data-item="ai-grok-temperature" data-type="value" data-ai-grok-temperature="">
                  <div class="text">1.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-grok-presence" data-range-reset="0.0">
                    <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M9.00967 5.12761H11.0097C12.1142 5.12761 13.468 5.89682 14.0335 6.8457L16.5089 11H21.0097C21.562 11 22.0097 11.4477 22.0097 12C22.0097 12.5523 21.562 13 21.0097 13H16.4138L13.9383 17.1543C13.3729 18.1032 12.0191 18.8724 10.9145 18.8724H8.91454L12.4138 13H5.42485L3.99036 15.4529H1.99036L4.00967 12L4.00967 11.967L2.00967 8.54712H4.00967L5.44417 11H12.5089L9.00967 5.12761Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="-1" max="2" step="0.1" value="0.0" data-range="float" data-item="ai-grok-presence" data-type="value" data-ai-grok-presence="">
                  <div class="text">0.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-grok-frequency" data-range-reset="0.0">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M21.2635 2.29289C20.873 1.90237 20.2398 1.90237 19.8493 2.29289L18.9769 3.16525C17.8618 2.63254 16.4857 2.82801 15.5621 3.75165L4.95549 14.3582L10.6123 20.0151L21.2189 9.4085C22.1426 8.48486 22.338 7.1088 21.8053 5.99367L22.6777 5.12132C23.0682 4.7308 23.0682 4.09763 22.6777 3.70711L21.2635 2.29289ZM16.9955 10.8035L10.6123 17.1867L7.78392 14.3582L14.1671 7.9751L16.9955 10.8035ZM18.8138 8.98525L19.8047 7.99429C20.1953 7.60376 20.1953 6.9706 19.8047 6.58007L18.3905 5.16586C18 4.77534 17.3668 4.77534 16.9763 5.16586L15.9853 6.15683L18.8138 8.98525Z" fill="currentColor" />
                      <path d="M2 22.9502L4.12171 15.1717L9.77817 20.8289L2 22.9502Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="-2" max="2" step="0.1" value="0.0" data-range="float" data-item="ai-grok-frequency" data-type="value" data-ai-grok-frequency="">
                  <div class="text">0.0</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Limits"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-grok-dialog" data-range-reset="4">
                    <svg viewBox="-100 -100 1224 1224" xmlns="http://www.w3.org/2000/svg">
                      <path d="M170.65984 42.65984l682.65984 0q53.00224 0 90.50112 37.49888t37.49888 90.50112l0 469.34016q0 53.00224-37.49888 90.50112t-90.50112 37.49888l-341.34016 0-298.65984 213.34016 0-213.34016-42.65984 0q-53.00224 0-90.50112-37.49888t-37.49888-90.50112l0-469.34016q0-53.00224 37.49888-90.50112t90.50112-37.49888zM853.34016 128l-682.65984 0q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 469.34016q0 17.67424 12.4928 30.16704t30.16704 12.4928l128 0 0 132.99712 185.99936-132.99712 368.66048 0q17.67424 0 30.16704-12.4928t12.4928-30.16704l0-469.34016q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="2" max="10" step="2" value="4" data-range="dialog" data-item="ai-grok-dialog" data-type="value" data-ai-grok-dialog="">
                  <div class="text">4 sanomaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-grok-input" data-range-reset="2000">
                    <svg viewBox="-1 -1 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C9 2.34315 10.3431 1 12 1C13.6569 1 15 2.34315 15 4V12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12V4ZM13 4V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4Z" fill="currentColor" />
                      <path d="M18 12C18 14.973 15.8377 17.441 13 17.917V21H17V23H7V21H11V17.917C8.16229 17.441 6 14.973 6 12V9H8V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V9H18V12Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="2000" data-range="word" data-item="ai-grok-input" data-type="value" data-ai-grok-input="">
                  <div class="text">1000 sanaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-grok-output" data-range-reset="1000">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.939C14.2091 18.939 16 17.1482 16 14.939C16 12.7299 14.2091 10.939 12 10.939C9.79086 10.939 8 12.7299 8 14.939C8 17.1482 9.79086 18.939 12 18.939ZM12 16.939C13.1046 16.939 14 16.0436 14 14.939C14 13.8345 13.1046 12.939 12 12.939C10.8954 12.939 10 13.8345 10 14.939C10 16.0436 10.8954 16.939 12 16.939Z" fill="currentColor" />
                      <path d="M12 9.04401C13.1046 9.04401 14 8.14858 14 7.04401C14 5.93944 13.1046 5.04401 12 5.04401C10.8954 5.04401 10 5.93944 10 7.04401C10 8.14858 10.8954 9.04401 12 9.04401Z" fill="currentColor" />
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C5.34315 1 4 2.34315 4 4V20C4 21.6569 5.34315 23 7 23H17C18.6569 23 20 21.6569 20 20V4C20 2.34315 18.6569 1 17 1H7ZM17 3H7C6.44772 3 6 3.44772 6 4V20C6 20.5523 6.44772 21 7 21H17C17.5523 21 18 20.5523 18 20V4C18 3.44772 17.5523 3 17 3Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="1000" data-range="word" data-item="ai-grok-output" data-type="value" data-ai-grok-output="">
                  <div class="text">2000 sanaa</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-stop"></div>
              <input type="text" data-i18n-placeholder="ai-stopword" data-item="ai-grok-stop" data-type="value" data-ai-grok-stop="">
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-user"></div>
              <input type="text" data-i18n-placeholder="ai-username" data-item="ai-grok-user" data-type="value" data-ai-grok-user="">
            </div>
          </div>

          <div id="ai-llama" class="column">
            <div class="row">
              <div class="text label" data-i18n-text="System"></div>
              <textarea data-i18n-placeholder="ai-system" rows="1" autocapitalize="sentences" data-item="ai-openai-system" data-type="value" data-ai-openai-system=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example1"></div>
              <textarea data-i18n-placeholder="ai-user1" rows="1" autocapitalize="sentences" data-item="ai-openai-user1" data-type="value" data-ai-openai-user1=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai1" rows="1" autocapitalize="sentences" data-item="ai-openai-ai1" data-type="value" data-ai-openai-ai1=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example2"></div>
              <textarea data-i18n-placeholder="ai-user2" rows="1" autocapitalize="sentences" data-item="ai-openai-user2" data-type="value" data-ai-openai-user2=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai2" rows="1" autocapitalize="sentences" data-item="ai-openai-ai2" data-type="value" data-ai-openai-ai2=""></textarea>
            </div>

            <div class="vbar"></div>

            <div class="row">
              <div class="text label" data-i18n-text="Adjust"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-llama-temperature" data-range-reset="1.0">
                    <svg viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M336 972.8c-60.8-128-28.8-201.6 19.2-268.8 51.2-76.8 64-150.4 64-150.4s41.6 51.2 25.6 134.4c70.4-80 83.2-208 73.6-256 160 112 230.4 358.4 137.6 537.6 492.8-281.6 121.6-700.8 57.6-745.6 22.4 48 25.6 128-19.2 166.4-73.6-281.6-256-336-256-336 22.4 144-76.8 300.8-172.8 419.2-3.2-57.6-6.4-96-38.4-153.6-6.4 105.6-86.4 188.8-108.8 294.4C89.6 758.4 140.8 860.8 336 972.8L336 972.8z"  fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="0" max="2" step="0.1" value="1.0" data-range="float" data-item="ai-llama-temperature" data-type="value" data-ai-llama-temperature="">
                  <div class="text">1.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-llama-presence" data-range-reset="0.0">
                    <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M9.00967 5.12761H11.0097C12.1142 5.12761 13.468 5.89682 14.0335 6.8457L16.5089 11H21.0097C21.562 11 22.0097 11.4477 22.0097 12C22.0097 12.5523 21.562 13 21.0097 13H16.4138L13.9383 17.1543C13.3729 18.1032 12.0191 18.8724 10.9145 18.8724H8.91454L12.4138 13H5.42485L3.99036 15.4529H1.99036L4.00967 12L4.00967 11.967L2.00967 8.54712H4.00967L5.44417 11H12.5089L9.00967 5.12761Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="-1" max="2" step="0.1" value="0.0" data-range="float" data-item="ai-llama-presence" data-type="value" data-ai-llama-presence="">
                  <div class="text">0.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-llama-frequency" data-range-reset="0.0">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M21.2635 2.29289C20.873 1.90237 20.2398 1.90237 19.8493 2.29289L18.9769 3.16525C17.8618 2.63254 16.4857 2.82801 15.5621 3.75165L4.95549 14.3582L10.6123 20.0151L21.2189 9.4085C22.1426 8.48486 22.338 7.1088 21.8053 5.99367L22.6777 5.12132C23.0682 4.7308 23.0682 4.09763 22.6777 3.70711L21.2635 2.29289ZM16.9955 10.8035L10.6123 17.1867L7.78392 14.3582L14.1671 7.9751L16.9955 10.8035ZM18.8138 8.98525L19.8047 7.99429C20.1953 7.60376 20.1953 6.9706 19.8047 6.58007L18.3905 5.16586C18 4.77534 17.3668 4.77534 16.9763 5.16586L15.9853 6.15683L18.8138 8.98525Z" fill="currentColor" />
                      <path d="M2 22.9502L4.12171 15.1717L9.77817 20.8289L2 22.9502Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="-2" max="2" step="0.1" value="0.0" data-range="float" data-item="ai-llama-frequency" data-type="value" data-ai-llama-frequency="">
                  <div class="text">0.0</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Limits"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-llama-dialog" data-range-reset="4">
                    <svg viewBox="-100 -100 1224 1224" xmlns="http://www.w3.org/2000/svg">
                      <path d="M170.65984 42.65984l682.65984 0q53.00224 0 90.50112 37.49888t37.49888 90.50112l0 469.34016q0 53.00224-37.49888 90.50112t-90.50112 37.49888l-341.34016 0-298.65984 213.34016 0-213.34016-42.65984 0q-53.00224 0-90.50112-37.49888t-37.49888-90.50112l0-469.34016q0-53.00224 37.49888-90.50112t90.50112-37.49888zM853.34016 128l-682.65984 0q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 469.34016q0 17.67424 12.4928 30.16704t30.16704 12.4928l128 0 0 132.99712 185.99936-132.99712 368.66048 0q17.67424 0 30.16704-12.4928t12.4928-30.16704l0-469.34016q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="2" max="10" step="2" value="4" data-range="dialog" data-item="ai-llama-dialog" data-type="value" data-ai-llama-dialog="">
                  <div class="text">4 sanomaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-llama-input" data-range-reset="2000">
                    <svg viewBox="-1 -1 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C9 2.34315 10.3431 1 12 1C13.6569 1 15 2.34315 15 4V12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12V4ZM13 4V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4Z" fill="currentColor" />
                      <path d="M18 12C18 14.973 15.8377 17.441 13 17.917V21H17V23H7V21H11V17.917C8.16229 17.441 6 14.973 6 12V9H8V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V9H18V12Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="2000" data-range="word" data-item="ai-llama-input" data-type="value" data-ai-llama-input="">
                  <div class="text">1000 sanaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-llama-output" data-range-reset="1000">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.939C14.2091 18.939 16 17.1482 16 14.939C16 12.7299 14.2091 10.939 12 10.939C9.79086 10.939 8 12.7299 8 14.939C8 17.1482 9.79086 18.939 12 18.939ZM12 16.939C13.1046 16.939 14 16.0436 14 14.939C14 13.8345 13.1046 12.939 12 12.939C10.8954 12.939 10 13.8345 10 14.939C10 16.0436 10.8954 16.939 12 16.939Z" fill="currentColor" />
                      <path d="M12 9.04401C13.1046 9.04401 14 8.14858 14 7.04401C14 5.93944 13.1046 5.04401 12 5.04401C10.8954 5.04401 10 5.93944 10 7.04401C10 8.14858 10.8954 9.04401 12 9.04401Z" fill="currentColor" />
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C5.34315 1 4 2.34315 4 4V20C4 21.6569 5.34315 23 7 23H17C18.6569 23 20 21.6569 20 20V4C20 2.34315 18.6569 1 17 1H7ZM17 3H7C6.44772 3 6 3.44772 6 4V20C6 20.5523 6.44772 21 7 21H17C17.5523 21 18 20.5523 18 20V4C18 3.44772 17.5523 3 17 3Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="1000" data-range="word" data-item="ai-llama-output" data-type="value" data-ai-llama-output="">
                  <div class="text">2000 sanaa</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-stop"></div>
              <input type="text" data-i18n-placeholder="ai-stopword" data-item="ai-llama-stop" data-type="value" data-ai-llama-stop="">
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-user"></div>
              <input type="text" data-i18n-placeholder="ai-username" data-item="ai-llama-user" data-type="value" data-ai-llama-user="">
            </div>
          </div>

          <div id="ai-gemini" class="column">
            <div class="row">
              <div class="text label" data-i18n-text="System"></div>
              <textarea data-i18n-placeholder="ai-system" rows="1" autocapitalize="sentences" data-item="ai-gemini-system" data-type="value" data-ai-gemini-system=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example1"></div>
              <textarea data-i18n-placeholder="ai-user1" rows="1" autocapitalize="sentences" data-item="ai-gemini-user1" data-type="value" data-ai-gemini-user1=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai1" rows="1" autocapitalize="sentences" data-item="ai-gemini-ai1" data-type="value" data-ai-gemini-ai1=""></textarea>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Example2"></div>
              <textarea data-i18n-placeholder="ai-user2" rows="1" autocapitalize="sentences" data-item="ai-gemini-user2" data-type="value" data-ai-gemini-user2=""></textarea>
            </div>
            <div class="row">
              <div class="text label"></div>
              <textarea data-i18n-placeholder="ai-ai2" rows="1" autocapitalize="sentences" data-item="ai-gemini-ai2" data-type="value" data-ai-gemini-ai2=""></textarea>
            </div>

            <div class="vbar"></div>

            <div class="row">
              <div class="text label" data-i18n-text="Adjust"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-gemini-temperature" data-range-reset="1.0">
                    <svg viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path d="M336 972.8c-60.8-128-28.8-201.6 19.2-268.8 51.2-76.8 64-150.4 64-150.4s41.6 51.2 25.6 134.4c70.4-80 83.2-208 73.6-256 160 112 230.4 358.4 137.6 537.6 492.8-281.6 121.6-700.8 57.6-745.6 22.4 48 25.6 128-19.2 166.4-73.6-281.6-256-336-256-336 22.4 144-76.8 300.8-172.8 419.2-3.2-57.6-6.4-96-38.4-153.6-6.4 105.6-86.4 188.8-108.8 294.4C89.6 758.4 140.8 860.8 336 972.8L336 972.8z"  fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="0" max="1" step="0.1" value="1.0" data-range="float" data-item="ai-gemini-temperature" data-type="value" data-ai-gemini-temperature="">
                  <div class="text">1.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-gemini-topp" data-range-reset="1.0" style="display:block; text-align: center; width: 2rem;">P</div>
                  <input type="range" min="0" max="1" step="0.05" value="1.0" data-range="float" data-item="ai-gemini-topp" data-type="value" data-ai-gemini-topp="">
                  <div class="text">1.0</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-gemini-topk" data-range-reset="40" style="display:block; text-align: center; width: 2rem;">K</div>
                  <input type="range" min="1" max="40" step="1" value="40" data-range="float" data-item="ai-gemini-topk" data-type="value" data-ai-gemini-topk="">
                  <div class="text">40</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="Limits"></div>
              <div class="column">
                <div class="row">
                  <div class="command" data-target="ai-gemini-dialog" data-range-reset="4">
                    <svg viewBox="-100 -100 1224 1224" xmlns="http://www.w3.org/2000/svg">
                      <path d="M170.65984 42.65984l682.65984 0q53.00224 0 90.50112 37.49888t37.49888 90.50112l0 469.34016q0 53.00224-37.49888 90.50112t-90.50112 37.49888l-341.34016 0-298.65984 213.34016 0-213.34016-42.65984 0q-53.00224 0-90.50112-37.49888t-37.49888-90.50112l0-469.34016q0-53.00224 37.49888-90.50112t90.50112-37.49888zM853.34016 128l-682.65984 0q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 469.34016q0 17.67424 12.4928 30.16704t30.16704 12.4928l128 0 0 132.99712 185.99936-132.99712 368.66048 0q17.67424 0 30.16704-12.4928t12.4928-30.16704l0-469.34016q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="2" max="10" step="2" value="4" data-range="dialog" data-item="ai-gemini-dialog" data-type="value" data-ai-gemini-dialog="">
                  <div class="text">4 sanomaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-gemini-input" data-range-reset="2000">
                    <svg viewBox="-1 -1 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C9 2.34315 10.3431 1 12 1C13.6569 1 15 2.34315 15 4V12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12V4ZM13 4V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4Z" fill="currentColor" />
                      <path d="M18 12C18 14.973 15.8377 17.441 13 17.917V21H17V23H7V21H11V17.917C8.16229 17.441 6 14.973 6 12V9H8V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V9H18V12Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="2000" data-range="word" data-item="ai-gemini-input" data-type="value" data-ai-gemini-input="">
                  <div class="text">1000 sanaa</div>
                </div>
                <div class="row">
                  <div class="command" data-target="ai-gemini-output" data-range-reset="1000">
                    <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.939C14.2091 18.939 16 17.1482 16 14.939C16 12.7299 14.2091 10.939 12 10.939C9.79086 10.939 8 12.7299 8 14.939C8 17.1482 9.79086 18.939 12 18.939ZM12 16.939C13.1046 16.939 14 16.0436 14 14.939C14 13.8345 13.1046 12.939 12 12.939C10.8954 12.939 10 13.8345 10 14.939C10 16.0436 10.8954 16.939 12 16.939Z" fill="currentColor" />
                      <path d="M12 9.04401C13.1046 9.04401 14 8.14858 14 7.04401C14 5.93944 13.1046 5.04401 12 5.04401C10.8954 5.04401 10 5.93944 10 7.04401C10 8.14858 10.8954 9.04401 12 9.04401Z" fill="currentColor" />
                      <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C5.34315 1 4 2.34315 4 4V20C4 21.6569 5.34315 23 7 23H17C18.6569 23 20 21.6569 20 20V4C20 2.34315 18.6569 1 17 1H7ZM17 3H7C6.44772 3 6 3.44772 6 4V20C6 20.5523 6.44772 21 7 21H17C17.5523 21 18 20.5523 18 20V4C18 3.44772 17.5523 3 17 3Z" fill="currentColor" />
                    </svg>
                  </div>
                  <input type="range" min="100" max="4000" step="100" value="1000" data-range="word" data-item="ai-gemini-output" data-type="value" data-ai-gemini-output="">
                  <div class="text">2000 sanaa</div>
                </div>
              </div>
            </div>
            <div class="row">
              <div class="text label" data-i18n-text="ai-stop"></div>
              <input type="text" data-i18n-placeholder="ai-stopword" data-item="ai-gemini-stop" data-type="value" data-ai-gemini-stop="">
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Script"></div>
            <textarea id="script" data-i18n-placeholder="Script" data-item="script-text" data-type="value" data-script-text=""></textarea>
          </div>

        </div>

        <div id="right-view" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Location"></div>
            <div class="column">
              <div id="views" class="rowWrap">
                <label for="view-image-file" class="command" data-view-image="FILE">
                  <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z" fill="currentColor" />
                  </svg>
                </label>
                <input type="file" id="view-image-file" accept="image/*,video/*" style="display:none">
                <div class="command selected" data-i18n-text="Empty" data-item="view-image" data-type="option" data-view-image="NONE"></div>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="text label"></div>
            <div class="row fill">
              <div class="command" data-item="view-image" data-type="option" data-view-image="URL">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                  <path d="M14.8284 12L16.2426 13.4142L19.071 10.5858C20.6331 9.02365 20.6331 6.49099 19.071 4.9289C17.509 3.3668 14.9763 3.3668 13.4142 4.9289L10.5858 7.75732L12 9.17154L14.8284 6.34311C15.6095 5.56206 16.8758 5.56206 17.6568 6.34311C18.4379 7.12416 18.4379 8.39049 17.6568 9.17154L14.8284 12Z" fill="currentColor" />
                  <path d="M12 14.8285L13.4142 16.2427L10.5858 19.0711C9.02372 20.6332 6.49106 20.6332 4.92896 19.0711C3.36686 17.509 3.36686 14.9764 4.92896 13.4143L7.75739 10.5858L9.1716 12L6.34317 14.8285C5.56212 15.6095 5.56212 16.8758 6.34317 17.6569C7.12422 18.4379 8.39055 18.4379 9.1716 17.6569L12 14.8285Z" fill="currentColor" />
                  <path d="M14.8285 10.5857C15.219 10.1952 15.219 9.56199 14.8285 9.17147C14.4379 8.78094 13.8048 8.78094 13.4142 9.17147L9.1716 13.4141C8.78107 13.8046 8.78107 14.4378 9.1716 14.8283C9.56212 15.2188 10.1953 15.2188 10.5858 14.8283L14.8285 10.5857Z" fill="currentColor" />
                </svg>
              </div>
              <input class="fill" type="text" placeholder="URL" data-item="view-url" data-type="value">
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Adjust"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="view-brightness" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="2" step="0.05" value="1" data-item="view-brightness" data-type="value" data-range="percentage" data-view-brightness="brightness">
                <div class="text">100%</div>
              </div>
              <div class="row">
                <div class="command" data-target="view-contrast" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 18C10.4633 18 9.06151 17.4223 8 16.4722C6.7725 15.3736 6 13.777 6 12C6 10.223 6.7725 8.62643 8 7.52779C9.06151 6.57771 10.4633 6 12 6V18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="2" step="0.05" value="1" data-item="view-contrast" data-type="value" data-range="percentage" data-view-contrast="contrast">
                <div class="text">100%</div>
              </div>
              <div class="row">
                <div class="command" data-target="view-saturate" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M8.20348 2.00378C9.46407 2.00378 10.5067 3.10742 10.6786 4.54241L19.1622 13.0259L11.384 20.8041C10.2124 21.9757 8.31291 21.9757 7.14134 20.8041L2.8987 16.5615C1.72713 15.3899 1.72713 13.4904 2.8987 12.3188L5.70348 9.51404V4.96099C5.70348 3.32777 6.82277 2.00378 8.20348 2.00378ZM8.70348 4.96099V6.51404L7.70348 7.51404V4.96099C7.70348 4.63435 7.92734 4.36955 8.20348 4.36955C8.47963 4.36955 8.70348 4.63435 8.70348 4.96099ZM8.70348 10.8754V9.34247L4.31291 13.733C3.92239 14.1236 3.92239 14.7567 4.31291 15.1473L8.55555 19.3899C8.94608 19.7804 9.57924 19.7804 9.96977 19.3899L16.3337 13.0259L10.7035 7.39569V10.8754C10.7035 10.9184 10.7027 10.9612 10.7012 11.0038H8.69168C8.69941 10.9625 8.70348 10.9195 8.70348 10.8754Z" fill="currentColor" />
                    <path d="M16.8586 16.8749C15.687 18.0465 15.687 19.946 16.8586 21.1175C18.0302 22.2891 19.9297 22.2891 21.1013 21.1175C22.2728 19.946 22.2728 18.0465 21.1013 16.8749L18.9799 14.7536L16.8586 16.8749Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="2" step="0.05" value="1" data-item="view-saturate" data-type="value" data-range="percentage" data-view-saturate="saturate">
                <div class="text">100%</div>
              </div>
              <div class="row">
                <div class="command" data-target="view-blur" data-range-reset="0">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M4.70834 15.4397C5.92663 16.2928 7.3222 16.7038 8.70461 16.7063C9.17977 18.0045 10.0433 19.1753 11.2616 20.0284C14.4284 22.2458 18.7932 21.4762 21.0107 18.3093C23.2281 15.1425 22.4585 10.7777 19.2916 8.56024C18.0734 7.70719 16.6778 7.29621 15.2954 7.29371C14.8202 5.99552 13.9567 4.82467 12.7384 3.97161C9.57158 1.75417 5.20676 2.52381 2.98931 5.69064C0.771871 8.85748 1.54151 13.2223 4.70834 15.4397ZM5.8555 13.8014C6.6016 14.3239 7.44081 14.6142 8.28736 14.6875C8.20112 13.1725 8.60464 11.6187 9.54254 10.2793C10.4804 8.9398 11.8025 8.0292 13.2556 7.59209C12.8972 6.82166 12.3374 6.13235 11.5913 5.60992C9.32924 4.02603 6.21151 4.57577 4.62762 6.8378C3.04373 9.09982 3.59347 12.2176 5.8555 13.8014ZM18.1445 10.1985C17.3984 9.67611 16.5592 9.38581 15.7126 9.31251C15.7989 10.8275 15.3953 12.3813 14.4574 13.7207C13.5195 15.0602 12.1975 15.9708 10.7444 16.4079C11.1028 17.1783 11.6626 17.8676 12.4087 18.3901C14.6707 19.9739 17.7885 19.4242 19.3724 17.1622C20.9562 14.9002 20.4065 11.7824 18.1445 10.1985Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="4" step="0.2" value="0" data-item="view-blur" data-type="value" data-range="px" data-view-blur="blur">
                <div class="text">0 px</div>
              </div>
            </div>
          </div>

        </div>

        <div id="right-avatar" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Avatar"></div>
            <div class="column">
              <div id="avatars" class="rowWrap">
                <label for="avatar-name-file" class="command" data-avatar-name="FILE">
                  <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z" fill="currentColor" />
                  </svg>
                </label>
                <input type="file" id="avatar-name-file" accept=".glb" style="display:none">
              </div>
            </div>
          </div>
          <div class="row">
            <div class="text label"></div>
            <div class="row fill">
              <div class="command" data-item="avatar-name" data-type="option" data-avatar-name="URL">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                  <path d="M14.8284 12L16.2426 13.4142L19.071 10.5858C20.6331 9.02365 20.6331 6.49099 19.071 4.9289C17.509 3.3668 14.9763 3.3668 13.4142 4.9289L10.5858 7.75732L12 9.17154L14.8284 6.34311C15.6095 5.56206 16.8758 5.56206 17.6568 6.34311C18.4379 7.12416 18.4379 8.39049 17.6568 9.17154L14.8284 12Z" fill="currentColor" />
                  <path d="M12 14.8285L13.4142 16.2427L10.5858 19.0711C9.02372 20.6332 6.49106 20.6332 4.92896 19.0711C3.36686 17.509 3.36686 14.9764 4.92896 13.4143L7.75739 10.5858L9.1716 12L6.34317 14.8285C5.56212 15.6095 5.56212 16.8758 6.34317 17.6569C7.12422 18.4379 8.39055 18.4379 9.1716 17.6569L12 14.8285Z" fill="currentColor" />
                  <path d="M14.8285 10.5857C15.219 10.1952 15.219 9.56199 14.8285 9.17147C14.4379 8.78094 13.8048 8.78094 13.4142 9.17147L9.1716 13.4141C8.78107 13.8046 8.78107 14.4378 9.1716 14.8283C9.56212 15.2188 10.1953 15.2188 10.5858 14.8283L14.8285 10.5857Z" fill="currentColor" />
                </svg>
              </div>
              <input class="fill" type="text" placeholder="URL" data-item="avatar-url" data-type="value">
              <div class="command selected" data-item="avatar-body" data-type="option" data-avatar-body="F">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                  <path fill-rule="evenodd" clip-rule="evenodd" d="M12 3C9.23858 3 7 5.23858 7 8C7 10.419 8.71776 12.4367 11 12.9V15H8V17H11V21H13V17H16V15H13V12.9C15.2822 12.4367 17 10.419 17 8C17 5.23858 14.7614 3 12 3ZM9 8C9 9.65685 10.3431 11 12 11C13.6569 11 15 9.65685 15 8C15 6.34315 13.6569 5 12 5C10.3431 5 9 6.34315 9 8Z" fill="currentColor" />
                </svg>
              </div>
              <div class="command" data-item="avatar-body" data-type="option" data-avatar-body="M">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                  <path fill-rule="evenodd" clip-rule="evenodd" d="M12.1888 7L12.1909 5L19.1909 5.00746L19.1834 12.0075L17.1834 12.0053L17.1873 8.41678L14.143 11.4611C15.4612 13.4063 15.2587 16.0743 13.5355 17.7975C11.5829 19.7501 8.41709 19.7501 6.46447 17.7975C4.51184 15.8449 4.51184 12.6791 6.46447 10.7264C8.16216 9.02873 10.777 8.80709 12.7141 10.0615L15.7718 7.00382L12.1888 7ZM7.87868 12.1406C9.05025 10.9691 10.9497 10.9691 12.1213 12.1406C13.2929 13.3122 13.2929 15.2117 12.1213 16.3833C10.9497 17.5549 9.05025 17.5549 7.87868 16.3833C6.70711 15.2117 6.70711 13.3122 7.87868 12.1406Z" fill="currentColor" />
                </svg>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Emotion"></div>
            <div class="rowWrap">
              <div class="command" data-i18n-text="Neutral" data-mood="neutral">Perus</div>
              <div class="command" data-i18n-text="Happy" data-mood="happy">Ilo</div>
              <div class="command" data-i18n-text="Angry" data-mood="angry">Viha</div>
              <div class="command" data-i18n-text="Sad" data-mood="sad">Suru</div>
              <div class="command" data-i18n-text="Fear" data-mood="fear">Pelko</div>
              <div class="command" data-i18n-text="Disgust" data-mood="disgust">Inho</div>
              <div class="command" data-i18n-text="Love" data-mood="love">Rakkaus</div>
              <div class="command" data-i18n-text="Sleep" data-mood="sleep">Uni</div>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Pose"></div>
            <div id="poses" class="rowWrap">
              <div id="pose-repeat" class="command selected">
                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M18.3701 7.99993L13.8701 10.598V8.99993H6.88989V12.9999H4.88989V6.99993H13.8701V5.40186L18.3701 7.99993Z" fill="currentColor" />
                  <path d="M10.1299 16.9999H19.1101V10.9999H17.1101V14.9999H10.1299V13.4019L5.62988 15.9999L10.1299 18.598V16.9999Z" fill="currentColor" />
                </svg>
              </div>
              <label for="pose-file" class="command" data-pose="FILE">
                <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                  <path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z" fill="currentColor" />
                </svg>
              </label>
              <input type="file" id="pose-file" accept=".fbx" style="display:none">
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Gesture"></div>
            <div id="gestures" class="rowWrap">
              <div id="gesture-repeat" class="command selected">
                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M18.3701 7.99993L13.8701 10.598V8.99993H6.88989V12.9999H4.88989V6.99993H13.8701V5.40186L18.3701 7.99993Z" fill="currentColor" />
                  <path d="M10.1299 16.9999H19.1101V10.9999H17.1101V14.9999H10.1299V13.4019L5.62988 15.9999L10.1299 18.598V16.9999Z" fill="currentColor" />
                </svg>
              </div>
              <div id="gesture-mirror" class="command">
                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M13 7H11V17H13V7Z" fill="currentColor" />
                  <path d="M5.99996 7.75732L7.41418 9.17154L5.58572 11L9.99996 11V13L5.58579 13L7.41418 14.8284L5.99996 16.2426L1.75732 12L5.99996 7.75732Z" fill="currentColor" />
                  <path d="M18 16.2427L16.5858 14.8285L18.4143 13H14V11L18.4142 11L16.5858 9.17161L18 7.75739L22.2427 12L18 16.2427Z" fill="currentColor" />
                </svg>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Action"></div>
            <div id="animations" class="rowWrap">
              <div id="animation-repeat" class="command selected">
                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M18.3701 7.99993L13.8701 10.598V8.99993H6.88989V12.9999H4.88989V6.99993H13.8701V5.40186L18.3701 7.99993Z" fill="currentColor" />
                  <path d="M10.1299 16.9999H19.1101V10.9999H17.1101V14.9999H10.1299V13.4019L5.62988 15.9999L10.1299 18.598V16.9999Z" fill="currentColor" />
                </svg>
              </div>
              <label for="animation-file" class="command" data-animation="FILE">
                <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                  <path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z" fill="currentColor" />
                </svg>
              </label>
              <input type="file" id="animation-file" accept=".fbx" style="display:none">
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Adjust"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="avatar-brightness" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="2" step="0.05" value="1" data-item="avatar-brightness" data-type="value" data-range="percentage" data-avatar-brightness="brightness">
                <div class="text">100%</div>
              </div>
              <div class="row">
                <div class="command" data-target="avatar-contrast" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 18C10.4633 18 9.06151 17.4223 8 16.4722C6.7725 15.3736 6 13.777 6 12C6 10.223 6.7725 8.62643 8 7.52779C9.06151 6.57771 10.4633 6 12 6V18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="2" step="0.05" value="1" data-item="avatar-contrast" data-type="value" data-range="percentage" data-avatar-contrast="contrast">
                <div class="text">100%</div>
              </div>
              <div class="row">
                <div class="command" data-target="avatar-saturate" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M8.20348 2.00378C9.46407 2.00378 10.5067 3.10742 10.6786 4.54241L19.1622 13.0259L11.384 20.8041C10.2124 21.9757 8.31291 21.9757 7.14134 20.8041L2.8987 16.5615C1.72713 15.3899 1.72713 13.4904 2.8987 12.3188L5.70348 9.51404V4.96099C5.70348 3.32777 6.82277 2.00378 8.20348 2.00378ZM8.70348 4.96099V6.51404L7.70348 7.51404V4.96099C7.70348 4.63435 7.92734 4.36955 8.20348 4.36955C8.47963 4.36955 8.70348 4.63435 8.70348 4.96099ZM8.70348 10.8754V9.34247L4.31291 13.733C3.92239 14.1236 3.92239 14.7567 4.31291 15.1473L8.55555 19.3899C8.94608 19.7804 9.57924 19.7804 9.96977 19.3899L16.3337 13.0259L10.7035 7.39569V10.8754C10.7035 10.9184 10.7027 10.9612 10.7012 11.0038H8.69168C8.69941 10.9625 8.70348 10.9195 8.70348 10.8754Z" fill="currentColor" />
                    <path d="M16.8586 16.8749C15.687 18.0465 15.687 19.946 16.8586 21.1175C18.0302 22.2891 19.9297 22.2891 21.1013 21.1175C22.2728 19.946 22.2728 18.0465 21.1013 16.8749L18.9799 14.7536L16.8586 16.8749Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="2" step="0.05" value="1" data-item="avatar-saturate" data-type="value" data-range="percentage" data-avatar-saturate="saturate">
                <div class="text">100%</div>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="PoseMovement"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="pose-movement" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M9 21.5L17.5 13L13 10L15 2.5L6.5 11L11 14L9 21.5Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="1" step="0.05" value="1" data-item="pose-movement" data-type="value" data-range="float" data-pose-movement="">
                <div class="text">1</div>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Visible"></div>
            <div id="skinnedmeshes" class="rowWrap">
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Emoji"></div>
            <div id="emojis" class="rowWrap">
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Morphs"></div>
            <div id="morphs" class="column"></div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Data"></div>
            <textarea id="morphdata" rows="1" readonly></textarea>
          </div>

        </div>

        <div id="right-bones" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Features"></div>
            <div class="rowWrap">
              <div id="bonesfeaturedynamic" class="command" data-i18n-text="DynamicBones"></div>
              <div id="bonesfeaturelimits" class="command" data-i18n-text="Limits"></div>
              <div id="bonesfeatureexcludes" class="command" data-i18n-text="Excludes"></div>
              <div id="bonesfeaturepivots" class="command" data-i18n-text="Pivots"></div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Helpers"></div>
            <div id="boneshelpers" class="rowWrap">
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Sensitivity"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="dynamicbones-sensitivity" data-range-reset="1">
                  <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 9V13.1707C9.83481 13.5825 9 14.6938 9 16C9 17.6569 10.3431 19 12 19C13.6569 19 15 17.6569 15 16C15 14.6938 14.1652 13.5825 13 13.1707V9H11ZM11 16C11 15.4477 11.4477 15 12 15C12.5523 15 13 15.4477 13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16Z" fill="currentColor" />
                    <path d="M12 5C15.866 5 19 8.13401 19 12V13H17V12C17 9.23858 14.7614 7 12 7C9.23858 7 7 9.23858 7 12V13H5V12C5 8.13401 8.13401 5 12 5Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="1.5" step="0.05" value="1" data-item="dynamicbones-sensitivity" data-type="value" data-range="float" data-dynamicbones-sensitivity="">
                <div class="text">1</div>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Movement"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="dynamicbones-movement" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M9 21.5L17.5 13L13 10L15 2.5L6.5 11L11 14L9 21.5Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="1.5" step="0.05" value="1" data-item="dynamicbones-movement" data-type="value" data-range="float" data-dynamicbones-movement="">
                <div class="text">1</div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Edit"></div>
            <div id="bonesselect" class="rowWrap">
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Type"></div>
            <div class="rowWrap">
              <div id="bonetypepoint" class="command bonetype selected">point</div>
              <div id="bonetypelink" class="command bonetype">link</div>
              <div id="bonetypemix1" class="command bonetype">mix1</div>
              <div id="bonetypemix2" class="command bonetype">mix2</div>
              <div id="bonetypefull" class="command bonetype">full</div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Stiffness"></div>
            <div class="column">
              <div class="row">
                <div class="command bonetester" style="width: 10px">x</div>
                <div id="stiffnesslock" class="command">
                  <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                    <path d="M14.8284 12L16.2426 13.4142L19.071 10.5858C20.6331 9.02365 20.6331 6.49099 19.071 4.9289C17.509 3.3668 14.9763 3.3668 13.4142 4.9289L10.5858 7.75732L12 9.17154L14.8284 6.34311C15.6095 5.56206 16.8758 5.56206 17.6568 6.34311C18.4379 7.12416 18.4379 8.39049 17.6568 9.17154L14.8284 12Z" fill="currentColor" />
                    <path d="M12 14.8285L13.4142 16.2427L10.5858 19.0711C9.02372 20.6332 6.49106 20.6332 4.92896 19.0711C3.36686 17.509 3.36686 14.9764 4.92896 13.4143L7.75739 10.5858L9.1716 12L6.34317 14.8285C5.56212 15.6095 5.56212 16.8758 6.34317 17.6569C7.12422 18.4379 8.39055 18.4379 9.1716 17.6569L12 14.8285Z" fill="currentColor" />
                    <path d="M14.8285 10.5857C15.219 10.1952 15.219 9.56199 14.8285 9.17147C14.4379 8.78094 13.8048 8.78094 13.4142 9.17147L9.1716 13.4141C8.78107 13.8046 8.78107 14.4378 9.1716 14.8283C9.56212 15.2188 10.1953 15.2188 10.5858 14.8283L14.8285 10.5857Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="stiffnessx" class="stiffness" type="range" min="0" max="1000" step="1" value="100" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">y</div>
                <input  id="stiffnessy" class="stiffness" type="range" min="0" max="1000" step="1" value="100" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">z</div>
                <input id="stiffnessz" class="stiffness" type="range" min="0" max="1000" step="1" value="100" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">t</div>
                <input  id="stiffnesst" class="stiffness" type="range" min="0" max="1000" step="1" value="100" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Damping"></div>
            <div class="column">
              <div class="row">
                <div class="command bonetester" style="width: 10px">x</div>
                <div id="dampinglock" class="command">
                  <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                    <path d="M14.8284 12L16.2426 13.4142L19.071 10.5858C20.6331 9.02365 20.6331 6.49099 19.071 4.9289C17.509 3.3668 14.9763 3.3668 13.4142 4.9289L10.5858 7.75732L12 9.17154L14.8284 6.34311C15.6095 5.56206 16.8758 5.56206 17.6568 6.34311C18.4379 7.12416 18.4379 8.39049 17.6568 9.17154L14.8284 12Z" fill="currentColor" />
                    <path d="M12 14.8285L13.4142 16.2427L10.5858 19.0711C9.02372 20.6332 6.49106 20.6332 4.92896 19.0711C3.36686 17.509 3.36686 14.9764 4.92896 13.4143L7.75739 10.5858L9.1716 12L6.34317 14.8285C5.56212 15.6095 5.56212 16.8758 6.34317 17.6569C7.12422 18.4379 8.39055 18.4379 9.1716 17.6569L12 14.8285Z" fill="currentColor" />
                    <path d="M14.8285 10.5857C15.219 10.1952 15.219 9.56199 14.8285 9.17147C14.4379 8.78094 13.8048 8.78094 13.4142 9.17147L9.1716 13.4141C8.78107 13.8046 8.78107 14.4378 9.1716 14.8283C9.56212 15.2188 10.1953 15.2188 10.5858 14.8283L14.8285 10.5857Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="dampingx" class="damping" type="range" min="0" max="100" step="1" value="10" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">y</div>
                <input  id="dampingy" class="damping" type="range" min="0" max="100" step="1" value="10" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">z</div>
                <input id="dampingz" class="damping" type="range" min="0" max="100" step="1" value="10" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">t</div>
                <input  id="dampingt" class="damping" type="range" min="0" max="100" step="1" value="10" data-range="float" style="width: 358px">
                <div class="text">100</div>
              </div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="External"></div>
            <div class="row">
              <div class="command disabled" style="width: 58px"></div>
              <input id="external" type="range" min="0" max="1" step="0.01" value="1" data-range="float" style="width: 358px">
              <div class="text">1</div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Limits"></div>
            <div class="column">
              <div class="row">
                <div class="command bonetester" style="width: 10px">x</div>
                <div id="limitsx1null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="limitsx1" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="text label" style="width: 10px"></div>
                <div id="limitsx2null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="limitsx2" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 10px">y</div>
                <div id="limitsy1null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input  id="limitsy1" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="text label" style="width: 10px"></div>
                <div id="limitsy2null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="limitsy2" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 10px">z</div>
                <div id="limitsz1null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="limitsz1" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="text label" style="width: 10px"></div>
                <div id="limitsz2null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input id="limitsz2" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 10px">t</div>
                <div id="limitst1null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input  id="limitst1" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
              <div class="row">
                <div class="text label" style="width: 10px"></div>
                <div id="limitst2null" class="command limitsnull">
                  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
                  </svg>
                </div>
                <input  id="limitst2" class="limits" type="range" min="-0.5" max="0.5" step="0.01" value="0" data-range="meter" style="width: 358px">
                <div class="text">null</div>
              </div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="DeltaL"></div>
            <div class="column">
              <div class="row">
                <div class="command bonetester" style="width: 58px">x</div>
                <input id="deltalx" class="deltal" type="range" min="-0.2" max="0.2" step="0.01" value="0" data-range="float" style="width: 358px">
                <div class="text">0</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">y</div>
                <input  id="deltaly" class="deltal" type="range" min="-0.2" max="0.2" step="0.01" value="0" data-range="float" style="width: 358px">
                <div class="text">0</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">z</div>
                <input id="deltalz" class="deltal" type="range" min="-0.2" max="0.2" step="0.01" value="0" data-range="float" style="width: 358px">
                <div class="text">0</div>
              </div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="DeltaW"></div>
            <div class="column">
              <div class="row">
                <div class="command bonetester" style="width: 58px">x</div>
                <input id="deltawx" class="deltaw" type="range" min="-0.2" max="0.2" step="0.01" value="0" data-range="float" style="width: 358px">
                <div class="text">0</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">y</div>
                <input  id="deltawy" class="deltaw" type="range" min="-0.2" max="0.2" step="0.01" value="0" data-range="float" style="width: 358px">
                <div class="text">0</div>
              </div>
              <div class="row">
                <div class="command bonetester" style="width: 58px">z</div>
                <input id="deltawz" class="deltaw" type="range" min="-0.2" max="0.2" step="0.01" value="0" data-range="float" style="width: 358px">
                <div class="text">0</div>
              </div>
            </div>
          </div>

          <div class="vspace"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Other"></div>
            <div class="rowWrap">
              <div id="pivot" class="command" data-i18n-text="Pivot"></div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Data"></div>
            <textarea id="bonesdata" rows="1" readonly></textarea>
          </div>

        </div>

        <div id="right-light" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="LightAmbient"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="light-ambient-intensity" data-range-reset="2">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="10" step="0.1" value="2" data-item="light-ambient-intensity" data-type="value" data-range="float" data-light-ambient-intensity="">
                <div class="text">2</div>
                <div class="filler"></div>
                <input type="color" value="#ffffff" data-item="light-ambient-color" data-type="value" data-light-ambient-color="light-ambient-color"/>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="LightDirect"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="light-direct-intensity" data-range-reset="30">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="100" step="1" value="30" data-item="light-direct-intensity" data-type="value" data-range="float" data-light-direct-intensity="">
                <div class="text">10</div>
                <div class="filler"></div>
                <input type="color" value="#8888aa" data-item="light-direct-color" data-type="value" data-light-direct-color="light-direct-color"/>
              </div>
              <div class="row">
                <div class="command" data-target="light-direct-phi" data-range-reset="1">
                  <svg viewBox="-2 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M5.33929 4.46777H7.33929V7.02487C8.52931 6.08978 10.0299 5.53207 11.6607 5.53207C15.5267 5.53207 18.6607 8.66608 18.6607 12.5321C18.6607 16.3981 15.5267 19.5321 11.6607 19.5321C9.51025 19.5321 7.58625 18.5623 6.30219 17.0363L7.92151 15.8515C8.83741 16.8825 10.1732 17.5321 11.6607 17.5321C14.4222 17.5321 16.6607 15.2935 16.6607 12.5321C16.6607 9.77065 14.4222 7.53207 11.6607 7.53207C10.5739 7.53207 9.56805 7.87884 8.74779 8.46777L11.3393 8.46777V10.4678H5.33929V4.46777Z" fill="currentColor" transform="scale(0.7 1.2)"/>
                  </svg>
                </div>
                <input type="range" min="0" max="6.4" step="0.1" value="1" data-item="light-direct-phi" data-type="value" data-range="radian" data-light-direct-phi="">
                <div class="text">1</div>
              </div>
              <div class="row">
                <div class="command" data-target="light-direct-theta" data-range-reset="2">
                  <svg viewBox="2 -2 26 22" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M13.1459 11.0499L12.9716 9.05752L15.3462 8.84977C14.4471 7.98322 13.2242 7.4503 11.8769 7.4503C9.11547 7.4503 6.87689 9.68888 6.87689 12.4503C6.87689 15.2117 9.11547 17.4503 11.8769 17.4503C13.6977 17.4503 15.2911 16.4771 16.1654 15.0224L18.1682 15.5231C17.0301 17.8487 14.6405 19.4503 11.8769 19.4503C8.0109 19.4503 4.87689 16.3163 4.87689 12.4503C4.87689 8.58431 8.0109 5.4503 11.8769 5.4503C13.8233 5.4503 15.5842 6.24474 16.853 7.52706L16.6078 4.72412L18.6002 4.5498L19.1231 10.527L13.1459 11.0499Z" fill="currentColor" transform="scale(1.2 0.7)"/>
                  </svg>
                </div>
                <input type="range" min="0" max="6.4" step="0.1" value="2" data-item="light-direct-theta" data-type="value" data-range="radian" data-light-direct-theta="">
                <div class="text">10</div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="LightSpot"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="light-spot-intensity" data-range-reset="0">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="100" step="1" value="0" data-item="light-spot-intensity" data-type="value" data-range="float" data-light-spot-intensity="">
                <div class="text">0</div>
                <div class="filler"></div>
                <input type="color" value="#8888aa" data-item="light-spot-color" data-type="value" data-light-spot-color="light-spot-color"/>
              </div>
              <div class="row">
                <div class="command" data-target="light-spot-phi" data-range-reset="0.1">
                  <svg viewBox="-2 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M5.33929 4.46777H7.33929V7.02487C8.52931 6.08978 10.0299 5.53207 11.6607 5.53207C15.5267 5.53207 18.6607 8.66608 18.6607 12.5321C18.6607 16.3981 15.5267 19.5321 11.6607 19.5321C9.51025 19.5321 7.58625 18.5623 6.30219 17.0363L7.92151 15.8515C8.83741 16.8825 10.1732 17.5321 11.6607 17.5321C14.4222 17.5321 16.6607 15.2935 16.6607 12.5321C16.6607 9.77065 14.4222 7.53207 11.6607 7.53207C10.5739 7.53207 9.56805 7.87884 8.74779 8.46777L11.3393 8.46777V10.4678H5.33929V4.46777Z" fill="currentColor" transform="scale(0.7 1.2)"/>
                  </svg>
                </div>
                <input type="range" min="0" max="6.4" step="0.1" value="0.1" data-item="light-spot-phi" data-type="value" data-range="radian" data-light-spot-phi="">
                <div class="text">0.1</div>
              </div>
              <div class="row">
                <div class="command" data-target="light-spot-theta" data-range-reset="4">
                  <svg viewBox="2 -2 26 22" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M13.1459 11.0499L12.9716 9.05752L15.3462 8.84977C14.4471 7.98322 13.2242 7.4503 11.8769 7.4503C9.11547 7.4503 6.87689 9.68888 6.87689 12.4503C6.87689 15.2117 9.11547 17.4503 11.8769 17.4503C13.6977 17.4503 15.2911 16.4771 16.1654 15.0224L18.1682 15.5231C17.0301 17.8487 14.6405 19.4503 11.8769 19.4503C8.0109 19.4503 4.87689 16.3163 4.87689 12.4503C4.87689 8.58431 8.0109 5.4503 11.8769 5.4503C13.8233 5.4503 15.5842 6.24474 16.853 7.52706L16.6078 4.72412L18.6002 4.5498L19.1231 10.527L13.1459 11.0499Z" fill="currentColor" transform="scale(1.2 0.7)"/>
                  </svg>
                </div>
                <input type="range" min="0" max="6.4" step="0.1" value="4" data-item="light-spot-theta" data-type="value" data-range="radian" data-light-spot-theta="">
                <div class="text">4</div>
              </div>
              <div class="row">
                <div class="command" data-target="light-spot-dispersion" data-range-reset="1">
                  <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M16 9C16 11.2091 14.2091 13 12 13C9.79086 13 8 11.2091 8 9C8 6.79086 9.79086 5 12 5C14.2091 5 16 6.79086 16 9ZM14 9C14 10.1046 13.1046 11 12 11C10.8954 11 10 10.1046 10 9C10 7.89543 10.8954 7 12 7C13.1046 7 14 7.89543 14 9Z" fill="currentColor" />
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1ZM3 12C3 14.0902 3.71255 16.014 4.90798 17.5417C6.55245 15.3889 9.14627 14 12.0645 14C14.9448 14 17.5092 15.3531 19.1565 17.4583C20.313 15.9443 21 14.0524 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12ZM12 21C9.84977 21 7.87565 20.2459 6.32767 18.9878C7.59352 17.1812 9.69106 16 12.0645 16C14.4084 16 16.4833 17.1521 17.7538 18.9209C16.1939 20.2191 14.1881 21 12 21Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="1" step="0.01" value="1" data-item="light-spot-dispersion" data-type="value" data-range="radian" data-light-spot-dispersion="">
                <div class="text">1</div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Data"></div>
            <textarea id="lightingdata" rows="1" readonly></textarea>
          </div>

        </div>

        <div id="right-camera" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Director"></div>
            <div class="rowWrap">
              <div id="pause" class="command" data-i18n-text="Pause"></div>
              <div id="autorotate" class="command" data-i18n-text="Panning"></div>
              <div id="slowdown" class="command" data-i18n-text="Slow-motion"></div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Framing"></div>
            <div class="column">
              <div class="rowWrap">
                <div class="command" data-i18n-text="Full" data-item="camera-frame" data-type="option" data-camera-frame="full"></div>
                <div class="command" data-i18n-text="Mid" data-item="camera-frame" data-type="option" data-camera-frame="mid"></div>
                <div class="command selected" data-i18n-text="Upper" data-item="camera-frame" data-type="option" data-camera-frame="upper"></div>
                <div class="command" data-i18n-text="Head" data-item="camera-frame" data-type="option" data-camera-frame="head"></div>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Adjust"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="camera-x" data-range-reset="0">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M13 7H11V17H13V7Z" fill="currentColor" />
                    <path d="M5.99996 7.75732L7.41418 9.17154L5.58572 11L9.99996 11V13L5.58579 13L7.41418 14.8284L5.99996 16.2426L1.75732 12L5.99996 7.75732Z" fill="currentColor" />
                    <path d="M18 16.2427L16.5858 14.8285L18.4143 13H14V11L18.4142 11L16.5858 9.17161L18 7.75739L22.2427 12L18 16.2427Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="-1" max="1" step="0.01" value="0" data-item="camera-x" data-type="value" data-range="meter" data-camera-x="x">
                <div class="text">0 m</div>
              </div>
              <div class="row">
                <div class="command" data-target="camera-y" data-range-reset="0">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M7 11L7 13H17V11H7Z" fill="currentColor" />
                    <path d="M7.75732 18L9.17154 16.5858L11 18.4143V14H13V18.4142L14.8284 16.5858L16.2426 18L12 22.2427L7.75732 18Z" fill="currentColor" />
                    <path d="M16.2427 5.99996L14.8285 7.41418L13 5.58572V9.99996H11L11 5.58579L9.17161 7.41418L7.75739 5.99996L12 1.75732L16.2427 5.99996Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="-1" max="1" step="0.01" value="0" data-item="camera-y" data-type="value" data-range="meter" data-camera-y="y">
                <div class="text">0 m</div>
              </div>
              <div class="row">
                <div class="command" data-target="camera-d" data-range-reset="0">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M3 3H9V5H5V9H3V3Z" fill="currentColor" />
                    <path d="M3 21H9V19H5V15H3V21Z" fill="currentColor" />
                    <path d="M15 21H21V15H19V19H15V21Z" fill="currentColor" />
                    <path d="M21 3H15V5H19V9H21V3Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="-10" max="10" step="0.1" value="0" data-item="camera-d" data-type="value" data-range="meter" data-camera-d="d">
                <div class="text">0 m</div>
              </div>
              <div class="row">
                <div class="command" data-target="camera-rotx" data-range-reset="0">
                  <svg viewBox="-2 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M5.33929 4.46777H7.33929V7.02487C8.52931 6.08978 10.0299 5.53207 11.6607 5.53207C15.5267 5.53207 18.6607 8.66608 18.6607 12.5321C18.6607 16.3981 15.5267 19.5321 11.6607 19.5321C9.51025 19.5321 7.58625 18.5623 6.30219 17.0363L7.92151 15.8515C8.83741 16.8825 10.1732 17.5321 11.6607 17.5321C14.4222 17.5321 16.6607 15.2935 16.6607 12.5321C16.6607 9.77065 14.4222 7.53207 11.6607 7.53207C10.5739 7.53207 9.56805 7.87884 8.74779 8.46777L11.3393 8.46777V10.4678H5.33929V4.46777Z" fill="currentColor" transform="scale(0.7 1.2)"/>
                  </svg>
                </div>
                <input type="range" min="-0.5" max="0.5" step="0.1" value="0" data-item="camera-rotx" data-type="value" data-range="radian" data-camera-rotx="rotx">
                <div class="text">0 rad</div>
              </div>
              <div class="row">
                <div class="command" data-target="camera-roty" data-range-reset="0">
                  <svg viewBox="2 -2 26 22" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M13.1459 11.0499L12.9716 9.05752L15.3462 8.84977C14.4471 7.98322 13.2242 7.4503 11.8769 7.4503C9.11547 7.4503 6.87689 9.68888 6.87689 12.4503C6.87689 15.2117 9.11547 17.4503 11.8769 17.4503C13.6977 17.4503 15.2911 16.4771 16.1654 15.0224L18.1682 15.5231C17.0301 17.8487 14.6405 19.4503 11.8769 19.4503C8.0109 19.4503 4.87689 16.3163 4.87689 12.4503C4.87689 8.58431 8.0109 5.4503 11.8769 5.4503C13.8233 5.4503 15.5842 6.24474 16.853 7.52706L16.6078 4.72412L18.6002 4.5498L19.1231 10.527L13.1459 11.0499Z" fill="currentColor" transform="scale(1.2 0.7)"/>
                  </svg>
                </div>
                <input type="range" min="-3.2" max="3.2" step="0.1" value="0" data-item="camera-roty" data-type="value" data-range="radian" data-camera-roty="roty">
                <div class="text">0 rad</div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="FPS"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="camera-fps" data-range-reset="30">
                  <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M6 7C5.44772 7 5 7.44772 5 8C5 8.55228 5.44772 9 6 9C6.55228 9 7 8.55228 7 8C7 7.44772 6.55228 7 6 7Z" fill="currentColor"/>
                    <path d="M17 8C17 7.44772 17.4477 7 18 7C18.5523 7 19 7.44772 19 8C19 8.55228 18.5523 9 18 9C17.4477 9 17 8.55228 17 8Z" fill="currentColor"/>
                    <path d="M6 11C5.44772 11 5 11.4477 5 12C5 12.5523 5.44772 13 6 13C6.55228 13 7 12.5523 7 12C7 11.4477 6.55228 11 6 11Z" fill="currentColor"/>
                    <path d="M17 12C17 11.4477 17.4477 11 18 11C18.5523 11 19 11.4477 19 12C19 12.5523 18.5523 13 18 13C17.4477 13 17 12.5523 17 12Z" fill="currentColor"/>
                    <path d="M6 15C5.44772 15 5 15.4477 5 16C5 16.5523 5.44772 17 6 17C6.55228 17 7 16.5523 7 16C7 15.4477 6.55228 15 6 15Z" fill="currentColor"/>
                    <path d="M17 16C17 15.4477 17.4477 15 18 15C18.5523 15 19 15.4477 19 16C19 16.5523 18.5523 17 18 17C17.4477 17 17 16.5523 17 16Z" fill="currentColor"/>
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C2.34315 3 1 4.34315 1 6V18C1 19.6569 2.34315 21 4 21H20C21.6569 21 23 19.6569 23 18V6C23 4.34315 21.6569 3 20 3H4ZM20 5H4C3.44772 5 3 5.44772 3 6V18C3 18.5523 3.44772 19 4 19H20C20.5523 19 21 18.5523 21 18V6C21 5.44772 20.5523 5 20 5Z" fill="currentColor"/>
                  </svg>
                </div>
                <input type="range" min="24" max="60" step="1" value="30" data-item="camera-fps" data-type="value" data-range="float" data-camera-fps="">
                <div class="text">30</div>
              </div>
            </div>
          </div>

        </div>

        <div id="right-audio" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Ambience"></div>
            <div class="column">
              <div id="music" class="rowWrap">
                <label for="voice-background-file" class="command" data-voice-background="FILE">
                  <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z" fill="currentColor" />
                  </svg>
                </label>
                <input type="file" id="voice-background-file" accept="audio/*" style="display:none">
                <div class="command selected" data-i18n-text="Silence" data-item="voice-background" data-type="option" data-voice-background="NONE"></div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Speech"></div>
            <div class="column">
              <div class="rowWrap">
                <div class="command selected" data-item="voice-type" data-type="option" data-voice-type="google">Google</div>
                <div class="command" data-item="voice-type" data-type="option" data-voice-type="microsoft">Microsoft</div>
                <div class="command" data-item="voice-type" data-type="option" data-voice-type="eleven">ElevenLabs</div>
              </div>
            </div>
          </div>
          <div id="voice-google" class="row">
            <div class="text label"></div>
            <div class="column">
              <div id="googlevoices" class="rowWrap">
              </div>
              <div class="row">
                <div class="command" data-target="voice-google-rate" data-range-reset="1">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M13 7H11V17H13V7Z" fill="currentColor" />
                    <path d="M5.99996 7.75732L7.41418 9.17154L5.58572 11L9.99996 11V13L5.58579 13L7.41418 14.8284L5.99996 16.2426L1.75732 12L5.99996 7.75732Z" fill="currentColor" />
                    <path d="M18 16.2427L16.5858 14.8285L18.4143 13H14V11L18.4142 11L16.5858 9.17161L18 7.75739L22.2427 12L18 16.2427Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0.3" max="4" step="0.1" value="1" data-item="voice-google-rate" data-type="value" data-range="float" data-voice-google-rate="rate">
                <div class="text">0</div>
              </div>
              <div class="row">
                <div class="command" data-target="voice-google-pitch" data-range-reset="0">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M7 11L7 13H17V11H7Z" fill="currentColor" />
                    <path d="M7.75732 18L9.17154 16.5858L11 18.4143V14H13V18.4142L14.8284 16.5858L16.2426 18L12 22.2427L7.75732 18Z" fill="currentColor" />
                    <path d="M16.2427 5.99996L14.8285 7.41418L13 5.58572V9.99996H11L11 5.58579L9.17161 7.41418L7.75739 5.99996L12 1.75732L16.2427 5.99996Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="-20" max="20" step="1" value="0" data-item="voice-google-pitch" data-type="value" data-range="float" data-voice-google-pitch="pitch">
                <div class="text">0</div>
              </div>
            </div>
          </div>
          <div id="voice-microsoft" class="row">
            <div class="text label"></div>
            <div class="column">
              <div id="microsoftvoices" class="rowWrap">
              </div>
            </div>
          </div>
          <div id="voice-eleven" class="row">
            <div class="text label"></div>
            <div class="column">
              <div id="elevenvoices" class="rowWrap">
              </div>
            </div>
          </div>
          <div id="lipsync" class="row">
            <div class="text label" data-i18n-text="Lip-sync"></div>
            <div class="column">
              <div class="rowWrap">
                <div class="command" data-i18n-text="de" data-item="voice-lipsync-lang" data-type="option" data-voice-lipsync-lang="de"></div>
                <div class="command selected" data-i18n-text="en" data-item="voice-lipsync-lang" data-type="option" data-voice-lipsync-lang="en"></div>
                <div class="command" data-i18n-text="fi" data-item="voice-lipsync-lang" data-type="option" data-voice-lipsync-lang="fi"></div>
                <div class="command" data-i18n-text="fr" data-item="voice-lipsync-lang" data-type="option" data-voice-lipsync-lang="fr"></div>
                <div class="command" data-i18n-text="lt" data-item="voice-lipsync-lang" data-type="option" data-voice-lipsync-lang="lt"></div>
              </div>
            </div>
          </div>
          <div id="excludes" class="row">
            <div class="text label" data-i18n-text="Exclude"></div>
            <div class="column">
              <div class="rowWrap">
                <div class="command" data-i18n-text="Italics" data-item="voice-exclude-italics" data-type="boolean"></div>
                <div class="command" data-i18n-text="Code" data-item="voice-exclude-code" data-type="boolean"></div>
              </div>
            </div>
          </div>
          <div class="row">
            <div class="text label" data-i18n-text="voice-test"></div>
            <div id="playtest" class="command">
              <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path fill-rule="evenodd" clip-rule="evenodd" d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21ZM12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23Z" fill="currentColor" />
                <path d="M16 12L10 16.3301V7.66987L16 12Z" fill="currentColor" />
              </svg>
            </div>
            <textarea data-i18n-placeholder="test-sentence" rows="1" autocapitalize="sentences" data-item="voice-test" data-type="value" data-voice-test=""></textarea>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Mixer"></div>
            <div class="column">
              <div class="row">
                <div class="command" data-target="voice-mixerbg" data-range-reset="0.5">
                  <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M12 8.95439L17.6352 7.82736C19.0102 7.55235 20 6.34501 20 4.94274C20 3.08637 18.3017 1.69406 16.4813 2.05813L12.4117 2.87206C11.0094 3.15252 10 4.38376 10 5.8138V13.8895C9.41165 13.5492 8.72857 13.3544 8 13.3544C5.79086 13.3544 4 15.1453 4 17.3544C4 19.5635 5.79086 21.3544 8 21.3544C10.2091 21.3544 12 19.5635 12 17.3544V8.95439ZM16.8736 4.01929L12.8039 4.83322C12.3365 4.92671 12 5.33712 12 5.8138V6.91479L17.2429 5.8662C17.6831 5.77816 18 5.39165 18 4.94274C18 4.34846 17.4563 3.90274 16.8736 4.01929ZM10 17.3544C10 16.2498 9.10457 15.3544 8 15.3544C6.89543 15.3544 6 16.2498 6 17.3544C6 18.459 6.89543 19.3544 8 19.3544C9.10457 19.3544 10 18.459 10 17.3544Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="10" step="0.1" value="0.5" data-item="voice-mixerbg" data-type="value" data-range="float" data-voice-mixerbg="">
                <div class="text">0.5</div>
              </div>
              <div class="row">
                <div class="command" data-target="voice-mixerspeech" data-range-reset="1">
                  <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" clip-rule="evenodd" d="M16 7C16 9.20914 14.2091 11 12 11C9.79086 11 8 9.20914 8 7C8 4.79086 9.79086 3 12 3C14.2091 3 16 4.79086 16 7ZM14 7C14 8.10457 13.1046 9 12 9C10.8954 9 10 8.10457 10 7C10 5.89543 10.8954 5 12 5C13.1046 5 14 5.89543 14 7Z" fill="currentColor" />
                    <path d="M16 15C16 14.4477 15.5523 14 15 14H9C8.44772 14 8 14.4477 8 15V21H6V15C6 13.3431 7.34315 12 9 12H15C16.6569 12 18 13.3431 18 15V21H16V15Z" fill="currentColor" />
                  </svg>
                </div>
                <input type="range" min="0" max="10" step="0.1" value="1" data-item="voice-mixerspeech" data-type="value" data-range="float" data-voice-mixerspeech="">
                <div class="text">1</div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Space"></div>
            <div class="column">
              <div id="impulses" class="rowWrap">
                <div class="command selected" data-i18n-text="Dry" data-item="voice-reverb" data-type="option" data-voice-reverb="NONE"></div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

            <div class="row">
              <div class="text label" data-i18n-text="Whisper"></div>
              <div class="column">
                <div class="rowWrap">
                  <div class="command selected" data-item="whisper-type" data-type="option" data-whisper-type="openai">OpenAI</div>
                  <div class="command" data-item="whisper-type" data-type="option" data-whisper-type="local">Local</div>
                </div>
              </div>
            </div>

            <div class="row">
              <div class="text label"></div>
              <div id="playmp3" class="command disabled">
                <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path fill-rule="evenodd" clip-rule="evenodd" d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21ZM12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23Z" fill="currentColor" />
                  <path d="M16 12L10 16.3301V7.66987L16 12Z" fill="currentColor" />
                </svg>
              </div>
              <label for="loadmp3-file" id="loadmp3" class="command">
                <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
                  <path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z" fill="currentColor" />
                </svg>
              </label>
              <input type="file" id="loadmp3-file" accept=".m4a,.mp3,.webm,.mp4,.mpga,.wav,.mpeg" style="display:none">
              <input id="jsonmp3" class="disabled" disabled="disabled" type="text" placeholder="m4a/mp3/webm/mp4/mpga/wav/mpeg">
            </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label">Input</div>
            <textarea id="visemeword" placeholder="Test word." rows="1"></textarea>
          </div>

          <div class="row">
            <div class="text label">Output</div>
            <textarea id="visemedata" placeholder="Viseme data." rows="1" readonly></textarea>
          </div>

        </div>

        <div id="right-sessions" class="page">
          <div id="sessions">
            <div id="session-0" class="session selected"></div>
            <div id="session-1" class="session"></div>
          </div>
        </div>

        <div id="right-session" class="page noselect nodrag hidden">

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Session"></div>
            <div id="directory" class="column fill">
              <div id="entry-0" class="entry row">
                <div class="command fill selected" data-session="0">Nimet&ouml;n</div>
                <div class="command" data-entry-move="up">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M17.6569 16.2427L19.0711 14.8285L12.0001 7.75739L4.92896 14.8285L6.34317 16.2427L12.0001 10.5858L17.6569 16.2427Z" fill="currentColor" />
                  </svg>
                </div>
                <div class="command" data-entry-move="down">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M6.34317 7.75732L4.92896 9.17154L12 16.2426L19.0711 9.17157L17.6569 7.75735L12 13.4142L6.34317 7.75732Z" fill="currentColor" />
                  </svg>
                </div>
              </div>
              <div id="entry-1" class="entry row">
                <div class="command fill" data-session="1">Nimet&ouml;n</div>
                <div class="command" data-entry-move="up">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M17.6569 16.2427L19.0711 14.8285L12.0001 7.75739L4.92896 14.8285L6.34317 16.2427L12.0001 10.5858L17.6569 16.2427Z" fill="currentColor" />
                  </svg>
                </div>
                <div class="command" data-entry-move="down">
                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M6.34317 7.75732L4.92896 9.17154L12 16.2426L19.0711 9.17157L17.6569 7.75735L12 13.4142L6.34317 7.75732Z" fill="currentColor" />
                  </svg>
                </div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="Title"></div>
            <input class="fill" type="text" data-i18n-placeholder="Name" data-item="name" data-type="value">
            <div id="session-duplicate" class="command">
              <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M19 5H7V3H21V17H19V5Z" fill="currentColor" />
                <path d="M9 13V11H11V13H13V15H11V17H9V15H7V13H9Z" fill="currentColor" />
                <path fill-rule="evenodd" clip-rule="evenodd" d="M3 7H17V21H3V7ZM5 9H15V19H5V9Z" fill="currentColor" />
              </svg>
            </div>
            <div id="session-delete" class="command">
              <svg viewBox="-1 -1 26 26" xmlns="http://www.w3.org/2000/svg">
                <path fill-rule="evenodd" clip-rule="evenodd" d="M17 5V4C17 2.89543 16.1046 2 15 2H9C7.89543 2 7 2.89543 7 4V5H4C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H5V18C5 19.6569 6.34315 21 8 21H16C17.6569 21 19 19.6569 19 18V7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H17ZM15 4H9V5H15V4ZM17 7H7V18C7 18.5523 7.44772 19 8 19H16C16.5523 19 17 18.5523 17 18V7Z" fill="currentColor" />
                <path d="M9 9H11V17H9V9Z" fill="currentColor" />
                <path d="M13 9H15V17H13V9Z" fill="currentColor" />
              </svg>
            </div>
          </div>

          <div class="row">
            <div class="text label" data-i18n-text="Theme"></div>
            <div class="column">
              <div id="languages" class="rowWrap">
              </div>
              <div class="rowWrap">
                <div class="command selected" data-i18n-text="Dark" data-item="theme-brightness" data-type="option" data-theme-brightness="dark"></div>
                <div class="command" data-i18n-text="Light" data-item="theme-brightness" data-type="option" data-theme-brightness="light"></div>
              </div>
              <div class="rowWrap">
                <div class="command selected" data-i18n-text="theme-wide" data-item="theme-ratio" data-type="option" data-theme-ratio="wide"></div>
                <div class="command" data-i18n-text="theme-43" data-item="theme-ratio" data-type="option" data-theme-ratio="normal"></div>
              </div>
              <div class="rowWrap">
                <div class="command selected" data-i18n-text="theme-landscape" data-item="theme-layout" data-type="option" data-theme-layout="land"></div>
                <div class="command" data-i18n-text="theme-portrait" data-item="theme-layout" data-type="option" data-theme-layout="port"></div>
                <div class="command" data-i18n-text="theme-full" data-item="theme-layout" data-type="option" data-theme-layout="full"></div>
              </div>
            </div>
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text label" data-i18n-text="JSON"></div>
            <input id="json" type="text" placeholder="JSON">
          </div>

          <div class="vbar"></div>

          <div class="row">
            <div class="text">Note: API keys are not stored for security reasons.</div>
          </div>

          <div class="row">
            <div class="text label" style="width: 8rem">OpenAI</div>
            <input id="apikey-openai" class="apikey" type="text" placeholder="API key not specified; using proxy/JWT">
          </div>

          <div class="row">
            <div class="text label" style="width: 8rem">Gemini</div>
            <input id="apikey-gemini" class="apikey" type="text" placeholder="API key not specified; using proxy/JWT">
          </div>

          <div class="row">
            <div class="text label" style="width: 8rem">Grok</div>
            <input id="apikey-grok" class="apikey" type="text" placeholder="API key not specified; using proxy/JWT">
          </div>

          <div class="row">
            <div class="text label" style="width: 8rem">GoogleTTS</div>
            <input id="apikey-gtts" class="apikey" type="text" placeholder="API key not specified; using proxy/JWT">
          </div>

          <div class="row">
            <div class="text label" style="width: 8rem">MicrosoftTTS</div>
            <input id="apikey-microsoft" class="apikey" type="text" placeholder="API key not specified; using proxy/JWT">
          </div>

          <div class="row">
            <div class="text label" style="width: 8rem">ElevenLabs</div>
            <input id="apikey-eleven" class="apikey" type="text" placeholder="API key not specified; using proxy/JWT">
          </div>

        </div>

      </div>

      <div class="border starttransparent"></div>
    </div>


    <div id="bottom">
      <div class="pages noselect nodrag">

        <div class="page fill">
          <div class="row fill">
            <textarea id="input" class="starttransparent"  data-i18n-placeholder="input" autocapitalize="sentences"></textarea>
          </div>
        </div>

        <div class="row" style="column-gap:0;">
          <div id="selectall" class="command starttransparent">
            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <path d="M10.5858 13.4142L7.75735 10.5858L6.34314 12L10.5858 16.2427L17.6568 9.1716L16.2426 7.75739L10.5858 13.4142Z" fill="currentColor" />
            </svg>
          </div>
          <div id="flag" class="command starttransparent">
            <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M7 8V13C7 15.7614 9.23858 18 12 18C14.7614 18 17 15.7614 17 13V8H7ZM5 4H19V13C19 16.866 15.866 20 12 20C8.13401 20 5 16.866 5 13V4Z" fill="currentColor" />
            </svg>
          </div>
          <div id="score" class="text starttransparent" style="margin-left:-8px;">0</div>
          <div id="scripttag" class="command starttransparent">
            <svg viewBox="-4 -4 32 32" xmlns="http://www.w3.org/2000/svg">
              <path d="M17 9H7V7H17V9Z" fill="currentColor" /><path d="M7 13H17V11H7V13Z" fill="currentColor" /><path fill-rule="evenodd" clip-rule="evenodd" d="M2 18V2H22V18H16V22H14C11.7909 22 10 20.2091 10 18H2ZM12 16V18C12 19.1046 12.8954 20 14 20V16H20V4H4V16H12Z" fill="currentColor" />
            </svg>
          </div>
          <div id="scriptstatus" class="text starttransparent" style="margin-left:-8px;">1/1</div>
          <div class="filler"></div>
          <div id="record" class="command starttransparent" data-i18n-title="Record">
            <svg viewBox="-2 -2 28 28" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C9 2.34315 10.3431 1 12 1C13.6569 1 15 2.34315 15 4V12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12V4ZM13 4V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4Z" fill="currentColor" />
              <path d="M18 12C18 14.973 15.8377 17.441 13 17.917V21H17V23H7V21H11V17.917C8.16229 17.441 6 14.973 6 12V9H8V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V9H18V12Z" fill="currentColor" />
            </svg>
          </div>
          <div id="motion" class="command starttransparent" data-i18n-title="Motion">
            <svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
              <path d="M 34.498047 3 C 32.014047 3 30 5.0150469 30 7.4980469 C 30 9.9830469 32.014047 12 34.498047 12 C 36.987047 12 39 9.9830469 39 7.4980469 C 39 5.0150469 36.987047 3 34.498047 3 z M 20 10 A 2 2 0 0 0 18.919922 10.316406 L 18.917969 10.316406 L 18.886719 10.322266 L 11.957031 15.294922 A 2 2 0 0 0 11 17 A 2 2 0 0 0 13 19 A 2 2 0 0 0 14.287109 18.527344 L 14.289062 18.529297 L 20.283203 14.21875 L 24.691406 15.619141 L 19.757812 23.660156 L 19.757812 23.664062 A 4 4 0 0 0 19 26 A 4 4 0 0 0 21.210938 29.572266 L 28.755859 34.226562 L 27.048828 43.009766 A 2.5 2.5 0 0 0 27 43.5 A 2.5 2.5 0 0 0 29.5 46 A 2.5 2.5 0 0 0 31.955078 43.962891 L 31.957031 43.962891 L 33.984375 33.289062 L 33.978516 33.273438 A 2 2 0 0 0 34 33 A 2 2 0 0 0 33.345703 31.523438 L 33.339844 31.507812 L 27.9375 26.720703 L 32.607422 17.486328 L 32.605469 17.482422 A 3 3 0 0 0 33 16 A 3 3 0 0 0 31.193359 13.248047 L 31.191406 13.244141 L 28.685547 11.849609 L 20.519531 10.066406 L 20.507812 10.068359 A 2 2 0 0 0 20 10 z M 34.212891 18.742188 L 31.972656 23.171875 L 33.181641 25.837891 L 33.193359 25.845703 C 33.513359 26.525703 34.199 27 35 27 L 42 27 C 43.105 27 44 26.105 44 25 C 44 23.895 43.105 23 42 23 L 36 23 L 34.212891 18.742188 z M 17.517578 28.414062 L 15.242188 32 L 7.5 32 C 6.119 32 5 33.119 5 34.5 C 5 35.881 6.119 37 7.5 37 L 15.931641 37 C 17.228641 37 18.445312 36.3705 19.195312 35.3125 L 21.480469 32.089844 C 20.653469 31.578844 20.055828 31.208125 20.048828 31.203125 C 18.904828 30.555125 18.031578 29.569062 17.517578 28.414062 z"  fill="currentColor"/>
            </svg>
          </div>
          <div id="copy" class="command starttransparent">
            <svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path d="M13 7H7V5H13V7Z" fill="currentColor" />
              <path d="M13 11H7V9H13V11Z" fill="currentColor" />
              <path d="M7 15H13V13H7V15Z" fill="currentColor" />
              <path fill-rule="evenodd" clip-rule="evenodd" d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z" fill="currentColor" />
            </svg>
          </div>
          <div id="delete" class="command starttransparent">
            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd" d="M17 5V4C17 2.89543 16.1046 2 15 2H9C7.89543 2 7 2.89543 7 4V5H4C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H5V18C5 19.6569 6.34315 21 8 21H16C17.6569 21 19 19.6569 19 18V7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H17ZM15 4H9V5H15V4ZM17 7H7V18C7 18.5523 7.44772 19 8 19H16C16.5523 19 17 18.5523 17 18V7Z" fill="currentColor" />
              <path d="M9 9H11V17H9V9Z" fill="currentColor" />
              <path d="M13 9H15V17H13V9Z" fill="currentColor" />
            </svg>
          </div>
        </div>
      </div>

      <div class="border starttransparent"></div>
    </div>

  </div>

</body>
</html>
